// 一、Object 对象的方法
// 1、实例方法
//   let o=new Object();
// 2、Object 的静态方法
//   Object.keys(obj)：用来遍历对象的属性，参数是对象，返回一个数组。该数组的成员都是该对象自身的（而不是继承的）所有属性名，比如length是不会出现在数组的。
//   Object.getOwnPropertyNames()：跟 keys 类似，返回结果一样，只有在涉及不可枚举属性是才返回不一样的结果，比如有 length。
//   1).对象属性模型的相关方法
//     Object.getOwnPropertyDescriptor()：获取某个属性的描述对象。
//     Object.defineProperty()：通过描述对象，定义某个属性。
//     Object.defineProperties()：通过描述对象，定义多个属性。
//   2）控制对象状态的方法
//     Object.preventExtensions()：防止对象扩展。
//     Object.isExtensible()：判断对象是否可扩展。
//     Object.seal()：禁止对象配置。
//     Object.isSealed()：判断一个对象是否可配置。
//     Object.freeze()：冻结一个对象。
//     Object.isFrozen()：判断一个对象是否被冻结。
//   3）原型链相关方法
//     Object.create()：该方法可以指定原型对象和属性，返回一个新的对象。
//     Object.getPrototypeOf()：获取对象的Prototype对象。
// 3、Object 
//   Object.prototype.valueOf()：返回当前对象对应的值。
//   Object.prototype.toString()：返回当前对象对应的字符串形式。
//     数值：返回[object Number]。
//     字符串：返回[object String]。
//     布尔值：返回[object Boolean]。
//     undefined：返回[object Undefined]。
//     null：返回[object Null]。
//     数组：返回[object Array]。
//     arguments 对象：返回[object Arguments]。
//     函数：返回[object Function]。
//     Error 对象：返回[object Error]。
//     Date 对象：返回[object Date]。
//     RegExp 对象：返回[object RegExp]。
//     其他对象：返回[object Object]。
//   Object.prototype.toLocaleString()：返回当前对象对应的本地字符串形式。
//   Object.prototype.hasOwnProperty()：判断某个属性是否为当前对象自身的属性，还是继承自原型对象的属性。
//   Object.prototype.isPrototypeOf()：判断当前对象是否为另一个对象的原型。
//   Object.prototype.propertyIsEnumerable()：判断某个属性是否可枚举。
///** ES6  Object扩展 */
// 4、数据结构本身的改变
//     4.1、属性的简洁表示法：ES6 允许在大括号里面，直接写入变量和函数，作为对象的属性和方法。这样的书写更加简洁。
//         const foo = 'bar';
//         const baz = {foo};
//         baz // {foo: "bar"}
//     4.2、属性名表达式：JavaScript 定义对象的属性，有两种方法。
//         // 方法一
//         obj.foo = true;
//         // 方法二
//         obj['a' + 'bc'] = 123;
//         上面代码的方法一是直接用标识符作为属性名，方法二是用表达式作为属性名，这时要将表达式放在方括号之内。
//     4.3、方法的 name 属性：函数的name属性，返回函数名。对象方法也是函数，因此也有name属性。
//         const person = {
//         sayName() {
//             console.log('hello!');
//         },
//         };
//         person.sayName.name   // "sayName"
//         上面代码中，方法的name属性返回函数名（即方法名）。
//     4.4、属性的可枚举性和遍历
//     （1）可枚举性：对象的每个属性都有一个描述对象（Descriptor），用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。
//         目前，有四个操作会忽略enumerable为false的属性。
//             for...in 循环：只遍历对象自身的和继承的可枚举的属性。
//             Object.keys()：返回对象自身的所有可枚举的属性的键名。
//             JSON.stringify()：只串行化对象自身的可枚举的属性。
//             Object.assign()： 忽略enumerable为false的属性，只拷贝对象自身的可枚举的属性。（es6）
//         另外，ES6 规定，所有 Class 的原型的方法都是不可枚举的。
//     （2）属性的遍历：ES6 一共有 5 种方法可以遍历对象的属性。
//         （a）for...in
//         for...in循环遍历对象自身的和继承的可枚举属性（不含 Symbol 属性）。
//         （b）Object.keys(obj)
//         Object.keys返回一个数组，包括对象自身的（不含继承的）所有可枚举属性（不含 Symbol 属性）的键名。
//         （c）Object.getOwnPropertyNames(obj)
//         Object.getOwnPropertyNames返回一个数组，包含对象自身的所有属性（不含 Symbol 属性，但是包括不可枚举属性）的键名。
//         （d）Object.getOwnPropertySymbols(obj)
//         Object.getOwnPropertySymbols返回一个数组，包含对象自身的所有 Symbol 属性的键名。
//         （e）Reflect.ownKeys(obj)
//         Reflect.ownKeys 返回一个数组，包含对象自身的（不含继承的）所有键名，不管键名是 Symbol 或字符串，也不管是否可枚举。
//         以上的 5 种方法遍历对象的键名，都遵守同样的属性遍历的次序规则。
//             首先遍历所有数值键，按照数值升序排列。
//             其次遍历所有字符串键，按照加入时间升序排列。
//             最后遍历所有 Symbol 键，按照加入时间升序排列。
//     4.5、super 关键字：我们知道，this关键字总是指向函数所在的当前对象，ES6 又新增了另一个类似的关键字super，指向当前对象的原型对象。
//     4.6、对象的扩展运算符
//     （1）解构赋值：对象的解构赋值用于从一个对象取值，相当于将目标对象自身的所有可遍历的（enumerable）、但尚未被读取的属性，
//         分配到指定的对象上面。所有的键和它们的值，都会拷贝到新对象上面。
//     （2）扩展运算符：对象的扩展运算符（...）用于取出参数对象的所有可遍历属性，拷贝到当前对象之中。
//     4.7、链判断运算符：?.
//         链判断运算符有三种用法。
//             obj?.prop // 对象属性
//             obj?.[expr] // 同上
//             func?.(...args) // 函数或对象方法的调用
//     （1）短路机制： ?.运算符相当于一种短路机制，只要不满足条件，就不再往下执行。
//         a?.[++x]
//         // 等同于
//         a == null ? undefined : a[++x]
//         上面代码中，如果a是undefined或null，那么x不会进行递增运算。
//     （2）delete 运算符
//         delete a?.b
//         // 等同于
//         a == null ? undefined : delete a.b
//         上面代码中，如果a是undefined或null，会直接返回undefined，而不会进行delete运算。
//     （3）括号的影响
//         如果属性链有圆括号，链判断运算符对圆括号外部没有影响，只对圆括号内部有影响。
//         (a?.b).c
//         // 等价于
//         (a == null ? undefined : a.b).c
//         上面代码中，?.对圆括号外部没有影响，不管a对象是否存在，圆括号后面的.c总是会执行。
//     （4）报错场合
//         以下写法是禁止的，会报错。
//         // 构造函数
//         new a?.()
//         new a?.b()
//         // 链判断运算符的右侧有模板字符串
//         a?.`{b}`
//         a?.b`{c}`
//         // 链判断运算符的左侧是 super
//         super?.()
//         super?.foo
//         // 链运算符用于赋值运算符左侧
//         a?.b = c
//     （5）右侧不得为十进制数值：为了保证兼容以前的代码，允许foo?.3:0被解析成foo ? .3 : 0，因此规定如果?.后面紧跟一个十进制数字，那么?.不再被看成是一个完整的运算符，
//         而会按照三元运算符进行处理，也就是说，那个小数点会归属于后面的十进制数字，形成一个小数。 
//     4.8、Null 判断运算符：??  
//         const enable = props.enabled ?? true;
///** ES6  Object新增的方法 */
// 5、新增的方法
//     5.1、Object.is()：ES6 提出“Same-value equality”（同值相等）算法，用来解决这个问题。Object.is就是部署这个算法的新方法。
//         它用来比较两个值是否严格相等，与严格比较运算符（===）的行为基本一致。
//     5.2、Object.assign()
//     （1）基本用法：Object.assign()方法用于对象的合并，将源对象（source）的所有可枚举属性，复制到目标对象（target）。
//     （2）注意点
//         （a）浅拷贝：Object.assign()方法实行的是浅拷贝，而不是深拷贝。也就是说，如果源对象某个属性的值是对象，那么目标对象拷贝得到的是这个对象的引用。
//         （b）同名属性的替换：对于这种嵌套的对象，一旦遇到同名属性，Object.assign()的处理方法是替换，而不是添加。
//         （c）数组的处理：Object.assign()可以用来处理数组，但是会把数组视为对象。
//         （d）取值函数的处理：Object.assign()只能进行值的复制，如果要复制的值是一个取值函数，那么将求值后再复制。
//     （3）常见用途：Object.assign()方法有很多用处。
//         （a）为对象添加属性
//         （b）为对象添加方法
//         （c）克隆对象
//         （d）合并多个对象：将多个对象合并到某个对象。
//         （e）为属性指定默认值
//     5.3、Object.getOwnPropertyDescriptors()：ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象（descriptor）。
//         ES2017 引入了Object.getOwnPropertyDescriptors()方法，返回指定对象所有自身属性（非继承属性）的描述对象。
//     5.4、__proto__属性，Object.setPrototypeOf()，Object.getPrototypeOf()：JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法。
//     （1）__proto__属性： __proto__属性（前后各两个下划线），用来读取或设置当前对象的原型对象（prototype）
//     （2）Object.setPrototypeOf()：Object.setPrototypeOf方法的作用与__proto__相同，用来设置一个对象的原型对象（prototype），返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
//     （3）Object.getPrototypeOf()：该方法与Object.setPrototypeOf方法配套，用于读取一个对象的原型对象。
//     5.5、Object.keys()，Object.values()，Object.entries()
//     （1）Object.keys()：ES5 引入了Object.keys方法，返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键名。
//         ES2017 引入了跟Object.keys配套的Object.values和Object.entries，作为遍历一个对象的补充手段，供for...of循环使用。
//     （2）Object.values()：Object.values方法返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键值。
//     （3）Object.entries()：Object.entries()方法返回一个数组，成员是参数对象自身的（不含继承的）所有可遍历（enumerable）属性的键值对数组。
//     5.6、Object.fromEntries()：Object.fromEntries()方法是Object.entries()的逆操作，用于将一个键值对数组转为对象。

// 二、属性描述对象
//   1、属性描述对象：{value：undefine(默认)、writable：true（默认，属性是否可写）、enumerable：true（该属性是否可便利）、configurable：true（可配置性）、
//        get：undefined（属性的取值函数）、set：undefined（属性的设置函数）}。
//   2、Object.getOwnPropertyDescriptor(obj,'属性')；获取属性的描述对象。描述对象
//   3、Object.getOwnPropertyNames(obj)；参数对象自身的全部属性。自身
//   4、Object.defineProperty()，Object.defineProperties() ：
//     Object.defineProperty()方法允许通过属性描述对象，定义或修改一个属性，然后返回修改后的对象，它的用法如下。
//      object：属性所在的对象
//      propertyName：字符串，表示属性名
//      attributesObject：属性描述对象
//     如果一次性定义或修改多个属性，可以使用Object.defineProperties()方法
//   5、Object.prototype.propertyIsEnumerable()：来判断某个属性是否可遍历，即判断是否是自身属性，当是继承属性时返回false。
//   6、元属性：value,writale,enumerable,configurable
//   7、存取器：get，set
//   8、对象的拷贝：
//   var extend = function (to, from) {
//     for (var property in from) {
//       if (!from.hasOwnProperty(property)) continue;
//       Object.defineProperty(
//         to,
//         property,
//         Object.getOwnPropertyDescriptor(from, property)
//       );
//     }
//     return to;
//   }
//   extend({}, { get a(){ return 1 } })
//   9、控制对象状态：有时需要冻结对象的读写状态，防止对象被改变。JavaScript 提供了三种冻结方法，最弱的一种是Object.preventExtensions，其次是Object.seal，最强的是Object.freeze。
//     9.1、Object.preventExtensions() 方法可以使得一个对象无法再添加新的属性
//     9.2、Object.isExtensible方法用于检查一个对象是否使用了Object.preventExtensions方法。也就是说，检查是否可以为一个对象添加属性
//     9.3、Object.seal方法使得一个对象既无法添加新属性，也无法删除旧属性
//     9.4、Object.isSealed方法用于检查一个对象是否使用了Object.seal方法
//     9.5、Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值，使得这个对象实际上变成了常量
//     9.6、Object.isFrozen方法用于检查一个对象是否使用了Object.freeze方法。
//     9.7、局限性：上面的三个方法锁定对象的可写性有一个漏洞：可以通过改变原型对象，来为对象增加属性。Object.getPrototypeOf();

// 三、Array 对象
// 1、构造函数：var arr = new Array(2);表示数组长度，Array(1,2,3,1)将返回一个数组。
// 2、静态方法：Array.isArray()返回一个布尔值，判断参数是否为数组。
// 3、实例方法
//  3.1、valueOf()，toString() ：valueOf()是所有对象都拥有的方法，表示该对象求值；数组的valueOf方法返回数组本身。
//  3.2、push()，pop()：
//   push方法用于在数组的末端添加一个或多个元素，并返回添加新元素后的数组长度。注意，该方法会改变原数组。
//   pop方法用于删除数组的最后一个元素，并返回该元素。注意，该方法会改变原数组。
//  3.3、shift()，unshift()：
//   shift()方法用于删除数组的第一个元素，并返回该元素。注意，该方法会改变原数组。
//   shift()方法可以遍历并清空一个数.
//  3.4、join()：join()方法以指定参数作为分隔符，将所有数组成员连接为一个字符串返回。如果不提供参数，默认用逗号分隔。
//  3.5、concat()：concat方法用于多个数组的合并。它将新数组的成员，添加到原数组成员的后部，然后返回一个新数组，原数组不变。
//  3.6、reverse()：reverse方法用于颠倒排列数组元素，返回改变后的数组。注意，该方法将改变原数组。
//  3.7、slice()：slice方法用于提取目标数组的一部分，返回一个新数组，原数组不变。
//  3.8、splice()：splice方法用于删除原数组的一部分成员，并可以在删除的位置添加新的数组成员，返回值是被删除的元素。注意，该方法会改变原数组。
//  3.9、sort()：sort方法对数组成员进行排序，默认是按照字典顺序排序。排序后，原数组将被改变。
//  3.10、map()：map方法将数组的所有成员依次传入参数函数，然后把每一次的执行结果组成一个新数组返回。
//  3.11、forEach()：forEach的用法与map方法一致，参数是一个函数，该函数同样接受三个参数：当前值、当前位置、整个数组。
//  3.12、filter()：filter方法用于过滤数组成员，满足条件的成员组成一个新数组返回。
//  3.13、some()，every()：这两个方法类似“断言”（assert），返回一个布尔值，表示判断数组成员是否符合某种条件。
//   它们接受一个函数作为参数，所有数组成员依次执行该函数。该函数接受三个参数：当前成员、当前位置和整个数组，然后返回一个布尔值。
//   some方法是只要一个成员的返回值是true，则整个some方法的返回值就是true，否则返回false。
//  3.14、reduce()，reduceRight()：reduce方法和reduceRight方法依次处理数组的每个成员，最终累计为一个值。它们的差别是，
//   reduce是从左到右处理（从第一个成员到最后一个成员），reduceRight则是从右到左（从最后一个成员到第一个成员），其他完全一样。
//  3.15、indexOf()，lastIndexOf() ：
//   indexOf方法返回给定元素在数组中第一次出现的位置，如果没有出现则返回-1。indexOf方法还可以接受第二个参数，表示搜索的开始位置。
//   lastIndexOf方法返回给定元素在数组中最后一次出现的位置，如果没有出现则返回-1。
//   注意，这两个方法不能用来搜索NaN的位置，即它们无法确定数组成员是否包含NaN。
//  3.16、链式使用：上面这些数组方法之中，有不少返回的还是数组，所以可以链式使用。
///**ES6 数组（Array）*/
// 4、扩展运算符
//     4.1、含义：扩展运算符（spread）是三个点（...）。它好比 rest 参数的逆运算，将一个数组转为用逗号分隔的参数序列。
//     4.2、替代函数的 apply 方法： 由于扩展运算符可以展开数组，所以不再需要apply方法，将数组转为函数的参数了。
//     4.3、扩展运算符的应用
//     （1）复制数组：数组是复合的数据类型，直接复制的话，只是复制了指向底层数据结构的指针，而不是克隆一个全新的数组。
//     （2）合并数组：扩展运算符提供了数组合并的新写法。
//     （3）与解构赋值结合：扩展运算符可以与解构赋值结合起来，用于生成数组。
//     （4）字符串：扩展运算符还可以将字符串转为真正的数组。
//     （5）实现了 Iterator 接口的对象：任何定义了遍历器（Iterator）接口的对象（参阅 Iterator 一章），都可以用扩展运算符转为真正的数组。
//     （6）Map 和 Set 结构，Generator 函数：
//         扩展运算符内部调用的是数据结构的 Iterator 接口，因此只要具有 Iterator 接口的对象，都可以使用扩展运算符，比如 Map 结构。
// 5、Array.from()
//     Array.from方法用于将两类对象转为真正的数组：类似数组的对象（array-like object）和可遍历（iterable）的对象（包括 ES6 新增的数据结构 Set 和 Map）。
// 6、Array.of()：Array.of() 方法用于将一组值，转换为数组。
// 7、数组实例的 copyWithin()
//     数组实例的copyWithin()方法，在当前数组内部，将指定位置的成员复制到其他位置（会覆盖原有成员），然后返回当前数组。也就是说，使用这个方法，会修改当前数组。
//     Array.prototype.copyWithin(target, start = 0, end = this.length)
//     它接受三个参数。
//         target（必需）：从该位置开始替换数据。如果为负值，表示倒数。
//         start（可选）：从该位置开始读取数据，默认为 0。如果为负值，表示从末尾开始计算。
//         end（可选）：到该位置前停止读取数据，默认等于数组长度。如果为负值，表示从末尾开始计算。
// 8、数组实例的 find() 和 findIndex()：
//     数组实例的find方法，用于找出第一个符合条件的数组成员。它的参数是一个回调函数，所有数组成员依次执行该回调函数，直到找出第一个返回值为true的成员，
//     然后返回该成员。如果没有符合条件的成员，则返回undefined。
// 9、数组实例的 fill()：fill方法使用给定值，填充一个数组。
//     ['a', 'b', 'c'].fill(7)
//     // [7, 7, 7]
//     new Array(3).fill(7)
//     // [7, 7, 7]
// 10、数组实例的 entries()，keys() 和 values()：
//     ES6 提供三个新的方法——entries()，keys()和values()——用于遍历数组。它们都返回一个遍历器对象（详见《Iterator》一章），可以用for...of循环进行遍历，
//     唯一的区别是 keys()是对键名的遍历、values()是对键值的遍历，entries()是对键值对的遍历。
// 11、数组实例的 includes()：Array.prototype.includes方法返回一个布尔值，表示某个数组是否包含给定的值，与字符串的includes方法类似。ES2016 引入了该方法。
// 12、数组实例的 flat()，flatMap()：数组的成员有时还是数组，Array.prototype.flat()用于将嵌套的数组“拉平”，变成一维的数组。该方法返回一个新数组，对原数据没有影响。
// 13、数组的空位：数组的空位指，数组的某一个位置没有任何值。比如，Array构造函数返回的数组都是空位。
//     Array(3) // [, , ,]
// 14、Array.prototype.sort() 的排序稳定性：排序稳定性（stable sorting）是排序算法的重要属性，指的是排序关键字相同的项目，排序前后的顺序不变。


// 四、包装对象
// 1、定义：对象是 JavaScript 语言最主要的数据类型，三种原始类型的值——数值、字符串、布尔值——在一定条件下，也会自动转为对象，也就是原始类型的“包装对象”（wrapper）。
//   所谓“包装对象”，指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成（包装成）对象。
//   var v1 = new Number(123); typeof v1; // "object"  v1 === 123; // false
// 2、实例方法：三种包装对象各自提供了许多实例方法，详见后文。这里介绍两种它们共同具有、从Object对象继承的方法：valueOf()和toString()。
//   2.1、valueOf()：方法返回包装对象实例对应的原始类型的值；
//   2.2、toString() ：方法返回对应的字符串形式
// 3、原始类型与实例对象的自动转换：某些场合，原始类型的值会自动当作包装对象调用，即调用包装对象的属性和方法。这时，JavaScript 引擎会自动将原始类型的值转为包装对象实例，并在使用后立刻销毁实例。
//   'abc'.length // 3
// 4、自定义方法：除了原生的实例方法，包装对象还可以自定义方法和属性，供原始类型的值直接调用
//   String.prototype.double = function () {
//     return this.valueOf() + this.valueOf();
//   };
//   'abc'.double()  // abcabc

//   Number.prototype.double = function () {
//     return this.valueOf() + this.valueOf();
//   };
//   (123).double() // 246

// 五、Boolean 对象
// 1、概述：Boolean对象是 JavaScript 的三个包装对象之一。作为构造函数，它主要用于生成布尔值的包装对象实例。
// 2、Boolean 函数的类型转换作用
//   Boolean(undefined) // false
//   Boolean(null) // false
//   Boolean(0) // false
//   Boolean('') // false
//   Boolean(NaN) // false

//   Boolean(1) // true
//   Boolean('false') // true
//   Boolean([]) // true
//   Boolean({}) // true
//   Boolean(function () {}) // true
//   Boolean(/foo/) // true
//   使用双重的否运算符（!）也可以将任意值转为对应的布尔值。
//   !!undefined // false
//   !!null // false
//   !!0 // false
//   !!'' // false
//   !!NaN // false

//   !!1 // true
//   !!'false' // true
//   !![] // true
//   !!{} // true
//   !!function(){} // true
//   !!/foo/ // true
//   对于一些特殊值，Boolean对象前面加不加new，会得到完全相反的结果
//   if (Boolean(false)) {
//     console.log('true');
//   } // 无输出

//   if (new Boolean(false)) {
//     console.log('true');
//   } // true

// 六、Number 对象
// 1、概述：Number对象是数值对应的包装对象，可以作为构造函数使用，也可以作为工具函数使用。
// 2、静态属性：Number对象拥有以下一些静态属性（即直接定义在Number对象上的属性，而不是定义在实例上的属性）。
//   Number.POSITIVE_INFINITY：正的无限，指向Infinity。
//   Number.NEGATIVE_INFINITY：负的无限，指向-Infinity。
//   Number.NaN：表示非数值，指向NaN。
//   Number.MIN_VALUE：表示最小的正数（即最接近0的正数，在64位浮点数体系中为5e-324），相应的，最接近0的负数为-Number.MIN_VALUE。
//   Number.MAX_SAFE_INTEGER：表示能够精确表示的最大整数，即9007199254740991。
//   Number.MIN_SAFE_INTEGER：表示能够精确表示的最小整数，即-9007199254740991。
// 3、实例方法：Number对象有4个实例方法，都跟将数值转换成指定格式有关。
//   3.1、Number.prototype.toString()：用来将一个数值转为字符串形式
//   3.2、Number.prototype.toFixed()：方法先将一个数转为指定位数的小数，然后返回这个小数对应的字符串
//   3.3、Number.prototype.toExponential()：方法用于将一个数转为科学计数法形式
//   3.4、Number.prototype.toPrecision()：方法用于将一个数转为指定位数的有效数字
//   3.5、Number.prototype.toLocaleString()：方法接受一个地区码作为参数，返回一个字符串，表示当前数字在该地区的当地书写形式。
// 4、自定义方法：与其他对象一样，Number.prototype对象上面可以自定义方法，被Number的实例继承。
///**ES6 的扩展*/
//5、二进制和八进制表示法：ES6 提供了二进制和八进制数值的新的写法，分别用前缀0b（或0B）和0o（或0O）表示。
//6、Number.isFinite(), Number.isNaN()：ES6 在Number对象上，新提供了Number.isFinite()和Number.isNaN()两个方法。
    // Number.isFinite()用来检查一个数值是否为有限的（finite），即不是Infinity。
    // Number.isNaN()用来检查一个值是否为NaN。
//7、Number.parseInt(), Number.parseFloat()：ES6 将全局方法parseInt()和parseFloat()，移植到Number对象上面，行为完全保持不变。
//8、Number.isInteger()：Number.isInteger()用来判断一个数值是否为整数。
//9、Number.EPSILON：ES6 在Number对象上面，新增一个极小的常量Number.EPSILON。根据规格，它表示 1 与大于 1 的最小浮点数之间的差。
//10、安全整数和 Number.isSafeInteger()：JavaScript 能够准确表示的整数范围在-2^53到2^53之间（不含两个端点），超过这个范围，无法精确表示这个值。

// 七、String 对象
// 1、概述：String对象是 JavaScript 原生提供的三个包装对象之一，用来生成字符串对象。
//  var s1 = 'abc';
//  var s2 = new String('abc');
//  typeof s1 // "string"
//  typeof s2 // "object"
//  s2.valueOf() // "abc"
// 2、静态方法
//   2.1、String.fromCharCode()：该方法的参数是一个或多个数值，代表 Unicode 码点，返回值是这些码点组成的字符串。String.fromCharCode(104, 101, 108, 108, 111) // "hello"
// 3、实例属性
//   3.1、String.prototype.length：字符串实例的length属性返回字符串的长度
// 4、实例方法
//   4.1、String.prototype.charAt()：方法返回指定位置的字符，参数是从0开始编号的位置。、
//   4.2、String.prototype.charCodeAt()：方法返回字符串指定位置的 Unicode 码点（十进制表示），相当于String.fromCharCode()的逆操作。
//   4.3、String.prototype.concat()：方法用于连接两个字符串，返回一个新字符串，不改变原字符串。
//   4.4、String.prototype.slice(): 方法用于从原字符串取出子字符串并返回，不改变原字符串。它的第一个参数是子字符串的开始位置，第二个参数是子字符串的结束位置（不含该位置）
//   4.5、String.prototype.substring()：方法用于从原字符串取出子字符串并返回，不改变原字符串，跟slice方法很相像。它的第一个参数表示子字符串的开始位置，第二个位置表示结束位置（返回结果不含该位置）。
//   4.6、String.prototype.substr()：方法用于从原字符串取出子字符串并返回，不改变原字符串，跟slice和substring方法的作用相同。
//   4.7、String.prototype.indexOf()，String.prototype.lastIndexOf()：
//    indexOf方法用于确定一个字符串在另一个字符串中第一次出现的位置，返回结果是匹配开始的位置。如果返回-1，就表示不匹配。
//    astIndexOf方法的用法跟indexOf方法一致，主要的区别是lastIndexOf从尾部开始匹配，indexOf则是从头部开始匹配。
//   4.8、String.prototype.trim()：方法用于去除字符串两端的空格，返回一个新字符串，不改变原字符串，该方法去除的不仅是空格，还包括制表符（\t、\v）、换行符（\n）和回车符（\r）。
//   4.9、String.prototype.toLowerCase()，String.prototype.toUpperCase() ：toLowerCase方法用于将一个字符串全部转为小写，toUpperCase则是全部转为大写。它们都返回一个新字符串，不改变原字符串。
//   4.10、String.prototype.match()：方法用于确定原字符串是否匹配某个子字符串，返回一个数组，成员为匹配的第一个字符串。如果没有找到匹配，则返回null。
//   4.11、String.prototype.search()，String.prototype.replace()：
//    search方法的用法基本等同于match，但是返回值为匹配的第一个位置。如果没有找到匹配，则返回-1；
//    replace方法用于替换匹配的子字符串，一般情况下只替换第一个匹配（除非使用带有g修饰符的正则表达式）。
//   4.12、String.prototype.split()：方法按照给定规则分割字符串，返回一个由分割出来的子字符串组成的数组
//   4.13、String.prototype.localeCompare()：localeCompare方法用于比较两个字符串。它返回一个整数，如果小于0，表示第一个字符串小于第二个字符串；如果等于0，表示两者相等；如果大于0，表示第一个字符串大于第二个字符串。
// /* es6 增加 */
//5、字符的 Unicode 表示法：ES6 加强了对 Unicode 的支持，允许采用\uxxxx形式表示一个字符
//6、字符串的遍历器接口: ES6 为字符串添加了遍历器接口 for...of
//7、直接输入 U+2028 和 U+2029：JavaScript 字符串允许直接输入字符，以及输入字符的转义形式。
//8、JSON.stringify() 的改造：根据标准，JSON 数据必须是 UTF-8 编码。但是，现在的JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串。
//9、模板字符串：``
//10、String.fromCodePoint()：ES5 提供String.fromCharCode()方法，用于从 Unicode 码点返回对应字符，但是这个方法不能识别码点大于0xFFFF的字符。
    // String.fromCharCode(0x20BB7)
    // // "ஷ"
    // 上面代码中，String.fromCharCode()不能识别大于0xFFFF的码点，所以0x20BB7就发生了溢出，最高位2被舍弃了，最后返回码点U+0BB7对应的字符，而不是码点U+20BB7对应的字符。
//11、String.raw()：ES6 还为原生的 String 对象，提供了一个raw()方法。该方法返回一个斜杠都被转义（即斜杠前面再加一个斜杠）的字符串，往往用于模板字符串的处理方法。
//12、实例方法：codePointAt()：JavaScript 内部，字符以 UTF-16 的格式储存，每个字符固定为2个字节。对于那些需要4个字节储存的字符（Unicode 码点大于0xFFFF的字符），JavaScript 会认为它们是两个字符。
//13、实例方法：normalize()：许多欧洲语言有语调符号和重音符号。为了表示它们，Unicode 提供了两种方法。一种是直接提供带重音符号的字符，比如Ǒ（\u01D1）。另一种是提供合成符号（combining character），
    //即原字符与重音符号的合成，两个字符合成一个字符，比如O（\u004F）和ˇ（\u030C）合成Ǒ（\u004F\u030C）
//14、实例方法：includes(), startsWith(), endsWith()：
    // 传统上，JavaScript 只有indexOf方法，可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
    // includes()：返回布尔值，表示是否找到了参数字符串。
    // startsWith()：返回布尔值，表示参数字符串是否在原字符串的头部。
    // endsWith()：返回布尔值，表示参数字符串是否在原字符串的尾部。
//15、实例方法：repeat()：repeat方法返回一个新字符串，表示将原字符串重复n次。
//16、实例方法：padStart()，padEnd()：ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度，会在头部或尾部补全。padStart()用于头部补全，padEnd()用于尾部补全。
//17、实例方法：trimStart()，trimEnd()：ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致，
    //trimStart()消除字符串头部的空格，trimEnd()消除尾部的空格。它们返回的都是新字符串，不会修改原始字符串。
//18、实例方法：matchAll()：matchAll()方法返回一个正则表达式在当前字符串的所有匹配，详见《正则的扩展》的一章。
//19、实例方法：replaceAll()：历史上，字符串的实例方法replace()只能替换第一个匹配。

// 八、Math 对象
// 是 JavaScript 的原生对象，提供各种数学功能。该对象不是构造函数，不能生成实例，所有的属性和方法都必须在Math对象上调用。
// 1、静态属性：
//     Math.E：常数e。
//     Math.LN2：2 的自然对数。
//     Math.LN10：10 的自然对数。
//     Math.LOG2E：以 2 为底的e的对数。
//     Math.LOG10E：以 10 为底的e的对数。
//     Math.PI：常数π。
//     Math.SQRT1_2：0.5 的平方根。
//     Math.SQRT2：2 的平方根。
// 2、静态方法：
//   2.1、Math.abs() 方法返回参数值的绝对值
//   2.2、Math.max()，Math.min():Math.max方法返回参数之中最大的那个值，Math.min返回最小的那个值。如果参数为空, Math.min返回Infinity, Math.max返回-Infinity
//   2.3、Math.floor()，Math.ceil()：Math.floor方法返回小于参数值的最大整数（地板值）；Math.ceil方法返回大于参数值的最小整数（天花板值）
//   2.4、Math.round()：方法用于四舍五入
//   2.5、Math.pow()：方法返回以第一个参数为底数、第二个参数为幂的指数值。
//   2.6、Math.sqrt()：方法返回参数值的平方根。如果参数是一个负值，则返回NaN
//   2.7、Math.log()：方法返回以e为底的自然对数值
//   2.8、Math.exp()：方法返回常数e的参数次方，Math.exp(1) // 2.718281828459045            Math.exp(3) // 20.085536923187668
//   2.9、Math.random()：返回0到1之间的一个伪随机数，可能等于0，但是一定小于1。
//   2.10、三角函数方法：
//     Math.sin()：返回参数的正弦（参数为弧度值）
//     Math.cos()：返回参数的余弦（参数为弧度值）
//     Math.tan()：返回参数的正切（参数为弧度值）
//     Math.asin()：返回参数的反正弦（返回值为弧度值）
//     Math.acos()：返回参数的反余弦（返回值为弧度值）
//     Math.atan()：返回参数的反正切（返回值为弧度值）
///**ES6*/
// 3、静态方法（17个，所有这些方法都是静态方法，只能在 Math 对象上调用。）
//     3.1、Math.trunc()：Math.trunc方法用于去除一个数的小数部分，返回整数部分。
//     3.2、Math.sign()：Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值，会先将其转换为数值。
//      它会返回五种值。
//         参数为正数，返回+1；
//         参数为负数，返回-1；
//         参数为 0，返回0；
//         参数为-0，返回-0;
//         其他值，返回NaN。
//     3.3、Math.cbrt()：Math.cbrt()方法用于计算一个数的立方根。
//     3.4、Math.clz32()：Math.clz32()方法将参数转为 32 位无符号整数的形式，然后返回这个 32 位值里面有多少个前导 0。
//     3.5、Math.imul()：Math.imul 方法返回两个数以 32 位带符号整数形式相乘的结果，返回的也是一个 32 位的带符号整数。
//     3.6、Math.fround()：Math.fround 方法返回一个数的32位单精度浮点数形式。
//     3.7、Math.hypot()：Math.hypot 方法返回所有参数的平方和的平方根。
//     3.8、对数方法
//     （1） Math.expm1()：Math.expm1(x)返回 ex - 1，即Math.exp(x) - 1。
//     （2）Math.log1p()：Math.log1p(x)方法返回1 + x的自然对数，即Math.log(1 + x)。如果x小于-1，返回NaN。
//     （3）Math.log10()：Math.log10(x)返回以 10 为底的x的对数。如果x小于 0，则返回 NaN。
//     （4）Math.log2()：Math.log2(x)返回以 2 为底的x的对数。如果x小于 0，则返回 NaN。
//     3.9、双曲函数方法：ES6 新增了 6 个双曲函数方法。
//         Math.sinh(x) 返回x的双曲正弦（hyperbolic sine）
//         Math.cosh(x) 返回x的双曲余弦（hyperbolic cosine）
//         Math.tanh(x) 返回x的双曲正切（hyperbolic tangent）
//         Math.asinh(x) 返回x的反双曲正弦（inverse hyperbolic sine）
//         Math.acosh(x) 返回x的反双曲余弦（inverse hyperbolic cosine）
//         Math.atanh(x) 返回x的反双曲正切（inverse hyperbolic tangent）
//4、指数运算符：ES2016 新增了一个指数运算符（**）。
//5、BigInt 数据类型
//     5.1、ES2020 引入了一种新的数据类型 BigInt（大整数），来解决这个问题，这是 ECMAScript 的第八种数据类型。
//         BigInt 只用来表示整数，没有位数的限制，任何位数的整数都可以精确表示。
//     5.2、BigInt 对象：JavaScript 原生提供BigInt对象，可以用作构造函数生成 BigInt 类型的数值。转换规则基本与Number()一致，将其他类型的值转为 BigInt。
//         BigInt 对象继承了 Object 对象的两个实例方法。
//         BigInt.prototype.toString()
//         BigInt.prototype.valueOf()
//     它还继承了 Number 对象的一个实例方法。
//         BigInt.prototype.toLocaleString()
//     此外，还提供了三个静态方法。
//         BigInt.asUintN(width, BigInt)： 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。
//         BigInt.asIntN(width, BigInt)：给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。
//         BigInt.parseInt(string[, radix])：近似于Number.parseInt()，将一个字符串转换成指定进制的 BigInt。
//     对于二进制数组，BigInt 新增了两个类型BigUint64Array和BigInt64Array，这两种数据类型返回的都是64位 BigInt。DataView对象的实例方法
//         DataView.prototype.getBigInt64()和DataView.prototype.getBigUint64()，返回的也是 BigInt。
//     5.3、转换规则：可以使用Boolean()、Number()和String()这三个方法，将 BigInt 可以转为布尔值、数值和字符串类型。
//         Boolean(0n) // false
//         Boolean(1n) // true
//         Number(1n)  // 1
//         String(1n)  // "1"
//     5.4、数学运算：数学运算方面，BigInt 类型的+、-、*和**这四个二元运算符，与 Number 类型的行为一致。除法运算/会舍去小数部分，返回一个整数。
//         9n / 5n
//         // 1n
//         几乎所有的数值运算符都可以用在 BigInt，但是有两个例外。
//             不带符号的右移位运算符>>>
//             一元的求正运算符+
//     5.5、其他运算：BigInt 对应的布尔值，与 Number 类型一致，即0n会转为false，其他值转为true。

// 九、Date 对象
// 对象是 JavaScript 原生的时间库。它以国际标准时间（UTC）1970年1月1日00:00:00作为时间的零点，可以表示的时间范围是前后各1亿天（单位为毫秒）
// 1、普通函数的用法：对象可以作为普通函数直接调用，返回一个代表当前时间的字符串，注意，即使带有参数，Date作为普通函数使用时，返回的还是当前时间
// 2、构造函数的用法：还可以当作构造函数使用。对它使用new命令，会返回一个Date对象的实例。如果不加参数，实例代表的就是当前时间。var today = new Date();
//   其他对象求值的时候，都是默认调用.valueOf()方法，但是Date实例求值的时候，默认调用的是toString()方法；
//   第一点，参数可以是负整数，代表1970年元旦之前的时间。
//   第二点，只要是能被Date.parse()方法解析的字符串，都可以当作参数。
//    new Date('2013-2-15')
//    new Date('2013/2/15')
//    new Date('02/15/2013')
//    new Date('2013-FEB-15')
//    new Date('FEB, 15, 2013')
//    new Date('FEB 15, 2013')
//    new Date('February, 15, 2013')
//    new Date('February 15, 2013')
//    new Date('15 Feb 2013')
//    new Date('15, February, 2013')
//    // Fri Feb 15 2013 00:00:00 GMT+0800 (CST)
//   第三，参数为年、月、日等多个整数时，年和月是不能省略的，其他参数都可以省略的。也就是说，这时至少需要两个参数，因为如果只使用“年”这一个参数，Date会将其解释为毫秒数
//    new Date(2013, 0, 1, 0, 0, 0, 0)
//    // Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
//    最后，各个参数的取值范围如下。
//     年：使用四位数年份，比如2000。如果写成两位数或个位数，则加上1900，即10代表1910年。如果是负数，表示公元前。
//     月：0表示一月，依次类推，11表示12月。
//     日：1到31。
//     小时：0到23。
//     分钟：0到59。
//     秒：0到59
//     毫秒：0到999。
//   注意，月份从0开始计算，但是，天数从1开始计算。另外，除了日期的默认值为1，小时、分钟、秒钟和毫秒的默认值都是0。
//   这些参数如果超出了正常范围，会被自动折算。比如，如果月设为15，就折算为下一年的4月。
// 3、日期的运算：类型自动转换时，Date实例如果转为数值，则等于对应的毫秒数；如果转为字符串，则等于对应的日期字符串。
//        所以，两个日期实例对象进行减法运算时，返回的是它们间隔的毫秒数；进行加法运算时，返回的是两个字符串连接而成的新字符串
//   var d1 = new Date(2000, 2, 1);
//   var d2 = new Date(2000, 3, 1);
//   d2 - d1
//   // 2678400000
//   d2 + d1
//   // "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)"
// 4、静态方法
//   4.1、Date.now()：方法返回当前时间距离时间零点（1970年1月1日 00:00:00 UTC）的毫秒数，相当于 Unix 时间戳乘以1000。Date.now() // 1364026285194
//   4.2、Date.parse()：方法用来解析日期字符串，返回该时间距离时间零点（1970年1月1日 00:00:00）的毫秒数。
//    日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准，即YYYY-MM-DDTHH:mm:ss.sssZ格式，其中最后的Z表示时区。但是，其他格式也可以被解析，如：
//     Date.parse('Aug 9, 1995')
//     Date.parse('January 26, 2011 13:51:50')
//     Date.parse('Mon, 25 Dec 1995 13:30:00 GMT')
//     Date.parse('Mon, 25 Dec 1995 13:30:00 +0430')
//     Date.parse('2011-10-10')
//     Date.parse('2011-10-10T14:48:00')
//    如果解析失败，返回NaN。
//   4.3、Date.UTC()：方法接受年、月、日等变量作为参数，返回该时间距离时间零点（1970年1月1日 00:00:00 UTC）的毫秒数
//     // 格式
//     Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
//     // 用法
//     Date.UTC(2011, 0, 1, 2, 3, 4, 567)
//     // 1293847384567
// 5、实例方法：Date的实例对象，有几十个自己的方法，除了valueOf和toString，可以分为以下三类。
//     to类：从Date对象返回一个字符串，表示指定的时间。
//     get类：获取Date对象的日期和时间。
//     set类：设置Date对象的日期和时间
//   5.1、Date.prototype.valueOf()：valueOf方法返回实例对象距离时间零点（1970年1月1日00:00:00 UTC）对应的毫秒数，该方法等同于getTime方法
//   5.2、to 类方法
//     5.2.1、Date.prototype.toString()：方法返回一个完整的日期字符串。
//     5.2.2、Date.prototype.toUTCString()：方法返回对应的 UTC 时间，也就是比北京时间晚8个小时
//     5.2.3、Date.prototype.toISOString()：toISOString方法返回对应时间的 ISO8601 写法
//       var d = new Date(2013, 0, 1);
//       d.toISOString()
//       // "2012-12-31T16:00:00.000Z"
//     5.2.4、Date.prototype.toJSON()：方法返回一个符合 JSON 格式的 ISO 日期字符串，与toISOString方法的返回结果完全相同
//     5.2.5、Date.prototype.toDateString()：方法返回日期字符串（不含小时、分和秒）
//     5.2.6、Date.prototype.toTimeString()：方法返回时间字符串（不含年月日）
//     5.2.7、本地时间：以下三种方法，可以将 Date 实例转为表示本地时间的字符串。
//       Date.prototype.toLocaleString()：完整的本地时间。
//       Date.prototype.toLocaleDateString()：本地日期（不含小时、分和秒）。
//       Date.prototype.toLocaleTimeString()：本地时间（不含年月日）。
//       下面是options的例子。
//       var d = new Date(2013, 0, 1);
//       // 时间格式
//       // 下面的设置是，星期和月份为完整文字，年份和日期为数字
//       d.toLocaleDateString('en-US', {
//         weekday: 'long',
//         year: 'numeric',
//         month: 'long',
//         day: 'numeric'
//       })
//       // "Tuesday, January 1, 2013"

//       // 指定时区
//       d.toLocaleTimeString('en-US', {
//         timeZone: 'UTC',
//         timeZoneName: 'short'
//       })
//       // "4:00:00 PM UTC"

//       d.toLocaleTimeString('en-US', {
//         timeZone: 'Asia/Shanghai',
//         timeZoneName: 'long'
//       })
//       // "12:00:00 AM China Standard Time"

//       // 小时周期为12还是24
//       d.toLocaleTimeString('en-US', {
//         hour12: false
//       })
//       // "00:00:00"

//       d.toLocaleTimeString('en-US', {
//         hour12: true
//       })
//       // "12:00:00 AM"
//     5.3、get 类方法
//       Date对象提供了一系列get*方法，用来获取实例对象某个方面的值。
//       getTime()：返回实例距离1970年1月1日00:00:00的毫秒数，等同于valueOf方法。
//       getDate()：返回实例对象对应每个月的几号（从1开始）。
//       getDay()：返回星期几，星期日为0，星期一为1，以此类推。
//       getFullYear()：返回四位的年份。
//       getMonth()：返回月份（0表示1月，11表示12月）。
//       getHours()：返回小时（0-23）。
//       getMilliseconds()：返回毫秒（0-999）。
//       getMinutes()：返回分钟（0-59）。
//       getSeconds()：返回秒（0-59）。
//       getTimezoneOffset()：返回当前时间与 UTC 的时区差异，以分钟表示，返回结果考虑到了夏令时因素。

//       所有这些get*方法返回的都是整数，不同方法返回值的范围不一样。
//       分钟和秒：0 到 59
//       小时：0 到 23
//       星期：0（星期天）到 6（星期六）
//       日期：1 到 31
//       月份：0（一月）到 11（十二月）

//       下面是一个例子，计算本年度还剩下多少天
//       function leftDays() {
//         var today = new Date();
//         var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999);
//         var msPerDay = 24 * 60 * 60 * 1000;
//         return Math.round((endYear.getTime() - today.getTime()) / msPerDay);
//       }
//       上面这些get*方法返回的都是当前时区的时间，Date对象还提供了这些方法对应的 UTC 版本，用来返回 UTC 时间。
//       getUTCDate()
//       getUTCFullYear()
//       getUTCMonth()
//       getUTCDay()
//       getUTCHours()
//       getUTCMinutes()
//       getUTCSeconds()
//       getUTCMilliseconds()
//     5.4、set 类方法
//       Date对象提供了一系列set*方法，用来设置实例对象的各个方面。
//       setDate(date)：设置实例对象对应的每个月的几号（1-31），返回改变后毫秒时间戳。
//       setFullYear(year [, month, date])：设置四位年份。
//       setHours(hour [, min, sec, ms])：设置小时（0-23）。
//       setMilliseconds()：设置毫秒（0-999）。
//       setMinutes(min [, sec, ms])：设置分钟（0-59）。
//       setMonth(month [, date])：设置月份（0-11）。
//       setSeconds(sec [, ms])：设置秒（0-59）。
//       setTime(milliseconds)：设置毫秒时间戳。

//       set*系列方法除了setTime()，都有对应的 UTC 版本，即设置 UTC 时区的时间。
//       setUTCDate()
//       setUTCFullYear()
//       setUTCHours()
//       setUTCMilliseconds()
//       setUTCMinutes()
//       setUTCMonth()
//       setUTCSeconds()

// 十、RegExp 对象
// RegExp 对象提供正则表达式的功能
// 1、概述：正则表达式（regular expression）是一种表达文本模式（即字符串结构）的方法，有点像字符串的模板，常常用来按照“给定模式”匹配文本。
//        比如，正则表达式给出一个 Email 地址的模式，然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。
//   新建正则表达式有两种方法。一种是使用字面量，以斜杠表示开始和结束。
//   var regex = /xyz/;

//   另一种是使用RegExp构造函数。
//   var regex = new RegExp('xyz');
// 2、实例属性:正则对象的实例属性分成两类。
//   一类是修饰符相关，用于了解设置了什么修饰符。
//     RegExp.prototype.ignoreCase：返回一个布尔值，表示是否设置了i修饰符。
//     RegExp.prototype.global：返回一个布尔值，表示是否设置了g修饰符。
//     RegExp.prototype.multiline：返回一个布尔值，表示是否设置了m修饰符。
//     RegExp.prototype.flags：返回一个字符串，包含了已经设置的所有修饰符，按字母排序。
//   上面四个属性都是只读的。

//   另一类是与修饰符无关的属性，主要是下面两个。
//     RegExp.prototype.lastIndex：返回一个整数，表示下一次开始搜索的位置。该属性可读写，但是只在进行连续搜索时有意义，详细介绍请看后文。
//     RegExp.prototype.source：返回正则表达式的字符串形式（不包括反斜杠），该属性只读。
// 3、实例方法：
//   3.1、RegExp.prototype.test()：正则实例对象的test方法返回一个布尔值，表示当前模式是否能匹配参数字符串。/cat/.test('cats and dogs') // true
//   3.2、RegExp.prototype.exec()：正则实例对象的exec()方法，用来返回匹配结果。如果发现匹配，就返回一个数组，成员是匹配成功的子字符串，否则返回null；
//    exec()方法的返回数组还包含以下两个属性：
//     input：整个原字符串。
//     index：模式匹配成功的开始位置（从0开始计数）。
// 4、字符串的实例方法：
//   字符串的实例方法之中，有4种与正则表达式有关。
//     String.prototype.match()：返回一个数组，成员是所有匹配的子字符串。
//     String.prototype.search()：按照给定的正则表达式进行搜索，返回一个整数，表示匹配开始的位置。
//     String.prototype.replace()：按照给定的正则表达式进行替换，返回替换后的字符串。
//       replace方法的第二个参数可以使用美元符号$，用来指代所替换的内容。
//         $&：匹配的子字符串。
//         $`：匹配结果前面的文本。
//         $'：匹配结果后面的文本。
//         $n：匹配成功的第n组内容，n是从1开始的自然数。
//         $$：指代美元符号$。
//     String.prototype.split()：按照给定规则进行字符串分割，返回一个数组，包含分割后的各个成员。
// 5、匹配规则
//   5.1、字面量字符和元字符
//   大部分字符在正则表达式中，就是字面的含义，比如/a/匹配a，/b/匹配b。如果在正则表达式之中，某个字符只表示它字面的含义（就像前面的a和b），那么它们就叫做“字面量字符”（literal characters）。
//   除了字面量字符以外，还有一部分字符有特殊含义，不代表字面的意思。它们叫做“元字符”（metacharacters），主要有以下几个。
//   （1）、点字符（.)：
//     点字符（.）匹配除回车（\r）、换行(\n) 、行分隔符（\u2028）和段分隔符（\u2029）以外的所有字符。注意，对于码点大于0xFFFF字符，点字符不能正确匹配，会认为这是两个字符。
//   （2）、位置字符：位置字符用来提示字符所处的位置，主要有两个字符。
//     ^ 表示字符串的开始位置
//     $ 表示字符串的结束位置
//   （3）、选择符（|）：
//     竖线符号（|）在正则表达式中表示“或关系”（OR），即cat|dog表示匹配cat或dog。
//   5.2、转义符：
//     正则表达式中那些有特殊含义的元字符，如果要匹配它们本身，就需要在它们前面要加上反斜杠。比如要匹配+，就要写成\+1。
//   5.3、特殊字符：
//    正则表达式对一些不能打印的特殊字符，提供了表达方法。
//     \cX 表示Ctrl-[X]，其中的X是A-Z之中任一个英文字母，用来匹配控制字符。
//     [\b] 匹配退格键(U+0008)，不要与\b混淆。
//     \n 匹配换行键。
//     \r 匹配回车键。
//     \t 匹配制表符 tab（U+0009）。
//     \v 匹配垂直制表符（U+000B）。
//     \f 匹配换页符（U+000C）。
//     \0 匹配null字符（U+0000）。
//     \xhh 匹配一个以两位十六进制数（\x00-\xFF）表示的字符。
//     \uhhhh 匹配一个以四位十六进制数（\u0000-\uFFFF）表示的 Unicode 字符。
//   5.4、字符类：
//    字符类（class）表示有一系列字符可供选择，只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内，比如[xyz] 表示x、y、z之中任选一个匹配。
//    （1）脱字符（^）：如果方括号内的第一个字符是[^]，则表示除了字符类之中的字符，其他字符都可以匹配。比如，[^xyz]表示除了x、y、z之外都可以匹配。
//    （2）连字符（-）：某些情况下，对于连续序列的字符，连字符（-）用来提供简写形式，表示字符的连续范围。比如，[abc]可以写成[a-c]，[0123456789]可以写成[0-9]，同理[A-Z]表示26个大写字母。
//   5.5、预定义模式：预定义模式指的是某些常见模式的简写方式。
//     \d 匹配0-9之间的任一数字，相当于[0-9]。
//     \D 匹配所有0-9以外的字符，相当于[^0-9]。
//     \w 匹配任意的字母、数字和下划线，相当于[A-Za-z0-9_]。
//     \W 除所有字母、数字和下划线以外的字符，相当于[^A-Za-z0-9_]。
//     \s 匹配空格（包括换行符、制表符、空格符等），相等于[ \t\r\n\v\f]。
//     \S 匹配非空格的字符，相当于[^ \t\r\n\v\f]。
//     \b 匹配词的边界。
//     \B 匹配非词边界，即在词的内部。
//   5.6、重复类：模式的精确匹配次数，使用大括号（{}）表示。{n}表示恰好重复n次，{n,}表示至少重复n次，{n,m}表示重复不少于n次，不多于m次。
//   5.7、量词符：量词符用来设定某个模式出现的次数。
//     ? 问号表示某个模式出现0次或1次，等同于{0, 1}。
//     * 星号表示某个模式出现0次或多次，等同于{0,}。
//     + 加号表示某个模式出现1次或多次，等同于{1,}。
//   5.8贪婪模式：上一小节的三个量词符，默认情况下都是最大可能匹配，即匹配到下一个字符不满足匹配规则为止。这被称为贪婪模式。
//    var s = 'aaa';
//    s.match(/a+/) // ["aaa"]
//    除了非贪婪模式的加号（+?），还有非贪婪模式的星号（*?）和非贪婪模式的问号（??）。
//     +?：表示某个模式出现1次或多次，匹配时采用非贪婪模式。
//     *?：表示某个模式出现0次或多次，匹配时采用非贪婪模式。
//     ??：表格某个模式出现0次或1次，匹配时采用非贪婪模式。
//   5.9、修饰符：
//    修饰符（modifier）表示模式的附加规则，放在正则模式的最尾部。
//    修饰符可以单个使用，也可以多个一起使用。
//    （1）g 修饰符：
//      默认情况下，第一次匹配成功后，正则对象就停止向下匹配了。g修饰符表示全局匹配（global），加上它以后，正则对象将匹配全部符合条件的结果，主要用于搜索和替换。
//    （2）i 修饰符：
//      默认情况下，正则对象区分字母的大小写，加上i修饰符以后表示忽略大小写（ignoreCase）。
//    （3）m 修饰符：
//      m修饰符表示多行模式（multiline），会修改^和$的行为。默认情况下（即不加m修饰符时），^和$匹配字符串的开始处和结尾处，加上m修饰符以后，^和$还会匹配行首和行尾，即^和$会识别换行符（\n）。
//   5.10、组匹配：
//   （1）概述：正则表达式的括号表示分组匹配，括号中的模式可以用来匹配分组的内容。
//   （2）非捕获组：(?:x)称为非捕获组（Non-capturing group），表示不返回该组匹配的内容，即匹配的结果中不计入这个括号。
//   （3）先行断言：
//     x(?=y)称为先行断言（Positive look-ahead），x只有在y前面才匹配，y不会被计入返回结果。比如，要匹配后面跟着百分号的数字，可以写成/\d+(?=%)/。
//     “先行断言”中，括号里的部分是不会返回的。
//     var m = 'abc'.match(/b(?=c)/);
//     m // ["b"]
//   （4）先行否定断言：
//     x(?!y)称为先行否定断言（Negative look-ahead），x只有不在y前面才匹配，y不会被计入返回结果。比如，要匹配后面跟的不是百分号的数字，就要写成/\d+(?!%)/。
//     /\d+(?!\.)/.exec('3.14'); // ["14"]
//     “先行否定断言”中，括号里的部分是不会返回的。
// /**ES6 */ 
//6、RegExp 构造函数
    // 在 ES5 中，RegExp构造函数的参数有两种情况。
    // 第一种情况是，参数是字符串，这时第二个参数表示正则表达式的修饰符（flag）。
    // var regex = new RegExp('xyz', 'i');
    // // 等价于
    // var regex = /xyz/i;
    // 第二种情况是，参数是一个正则表示式，这时会返回一个原有正则表达式的拷贝。
    // var regex = new RegExp(/xyz/i);
    // // 等价于
    // var regex = /xyz/i;
    // 但是，ES5 不允许此时使用第二个参数添加修饰符，否则会报错。
    // var regex = new RegExp(/xyz/, 'i');
//7、字符串的正则方法
    // ES6 出现之前，字符串对象共有 4 个方法，可以使用正则表达式：match()、replace()、search()和split()。
    // ES6 将这 4 个方法，在语言内部全部调用RegExp的实例方法，从而做到所有与正则相关的方法，全都定义在RegExp对象上。
    //     String.prototype.match 调用 RegExp.prototype[Symbol.match]
    //     String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
    //     String.prototype.search 调用 RegExp.prototype[Symbol.search]
    //     String.prototype.split 调用 RegExp.prototype[Symbol.split]
//8、u 修饰符：ES6 对正则表达式添加了u修饰符，含义为“Unicode 模式”，用来正确处理大于\uFFFF的 Unicode 字符。也就是说，会正确处理四个字节的 UTF-16 编码。
    // （1）点字符：点（.）字符在正则表达式中，含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符，点字符不能识别，必须加上u修饰符。
    // （2）Unicode 字符表示法：ES6 新增了使用大括号表示 Unicode 字符，这种表示法在正则表达式中必须加上u修饰符，才能识别当中的大括号，否则会被解读为量词。
    // （3）量词：使用u修饰符后，所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
    // （4）预定义模式：u修饰符也影响到预定义模式，能否正确识别码点大于0xFFFF的 Unicode 字符。
    // （5）i 修饰符：有些 Unicode 字符的编码不同，但是字型很相近，比如，\u004B与\u212A都是大写的K。
    // （6）转义：没有u修饰符的情况下，正则中没有定义的转义（如逗号的转义\,）无效，而在u模式会报错。
//9、RegExp.prototype.unicode 属性：正则实例对象新增unicode属性，表示是否设置了u修饰符。
//10、y 修饰符：除了u修饰符，ES6 还为正则表达式添加了y修饰符，叫做“粘连”（sticky）修饰符。
    // y修饰符的作用与g修饰符类似，也是全局匹配，后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于，g修饰符只要剩余位置中存在匹配就可，
    // 而y修饰符确保匹配必须从剩余的第一个位置开始，这也就是“粘连”的涵义。
//11、RegExp.prototype.sticky 属性：与y修饰符相匹配，ES6 的正则实例对象多了sticky属性，表示是否设置了y修饰符。
//12、RegExp.prototype.flags 属性：ES6 为正则表达式新增了flags属性，会返回正则表达式的修饰符。
//13、s 修饰符：dotAll 模式
    // 正则表达式中，点（.）是一个特殊字符，代表任意的单个字符，但是有两个例外。一个是四个字节的 UTF-16 字符，这个可以用u修饰符解决；
    //  另一个是行终止符（line terminator character）。
    // 所谓行终止符，就是该字符表示一行的终结。以下四个字符属于“行终止符”。
    //     U+000A 换行符（\n）
    //     U+000D 回车符（\r）
    //     U+2028 行分隔符（line separator）
    //     U+2029 段分隔符（paragraph separator）
//14、后行断言
    //JavaScript 语言的正则表达式，只支持先行断言（lookahead）和先行否定断言（negative lookahead），不支持后行断言（lookbehind）
    //  和后行否定断言（negative lookbehind）。ES2018 引入后行断言，V8 引擎 4.9 版（Chrome 62）已经支持。
    //“先行断言”指的是，x只有在y前面才匹配，必须写成/x(?=y)/。比如，只匹配百分号之前的数字，要写成/\d+(?=%)/。
    //  “先行否定断言”指的是，x只有不在y前面才匹配，必须写成/x(?!y)/。比如，只匹配不在百分号之前的数字，要写成/\d+(?!%)/。
//15、Unicode 属性类：ES2018 引入了一种新的类的写法\p{...}和\P{...}，允许正则表达式匹配符合 Unicode 某种属性的所有字符。
//16、正则匹配索引：正则匹配结果的开始位置和结束位置，目前获取并不是很方便。正则实例的exec()方法，返回结果有一个index属性，
    // 可以获取整个匹配结果的开始位置，但是如果包含组匹配，每个组匹配的开始位置，很难拿到。
//17、String.prototype.matchAll()：如果一个正则表达式在字符串里面有多个匹配，现在一般使用g修饰符或y修饰符，在循环里面逐一取出。


// 十一、JSON 对象
//   1、JSON 格式：JSON 对值的类型和格式有严格的规定。
//    复合类型的值只能是数组或对象，不能是函数、正则表达式对象、日期对象。
//    原始类型的值只有四种：字符串、数值（必须以十进制表示）、布尔值和null（不能使用NaN, Infinity, -Infinity和undefined）。
//    字符串必须使用双引号表示，不能使用单引号。
//    对象的键名必须放在双引号里面。
//    数组或对象最后一个成员的后面，不能加逗号。
//   2、JSON 对象：
//    JSON对象是 JavaScript 的原生对象，用来处理 JSON 格式数据。它有两个静态方法：JSON.stringify()和JSON.parse()。
//   3、JSON.stringify()：
//    3.1、基本用法：JSON.stringify()方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式，并且可以被JSON.parse()方法还原。
//    3.2、第二个参数：JSON.stringify()方法还可以接受一个数组，作为第二个参数，指定参数对象的哪些属性需要转成字符串。
//    3.3、第三个参数：JSON.stringify()还可以接受第三个参数，用于增加返回的 JSON 字符串的可读性。
//     默认返回的是单行字符串，对于大型的 JSON 对象，可读性非常差。第三个参数使得每个属性单独占据一行，并且将每个属性前面添加指定的前缀（不超过10个字符）。
//     // 分行输出
//     JSON.stringify({ p1: 1, p2: 2 }, null, '\t')
//     // {
//         // 	"p1": 1,
//         // 	"p2": 2
//     // }
//    3.4、// 分行输出
//     JSON.stringify({ p1: 1, p2: 2 }, null, '\t')
//     // {
//         // 	"p1": 1,
//         // 	"p2": 2
//     // }
//   4、JSON.parse()：JSON.parse()方法用于将 JSON 字符串转换成对应的值。

// 十二、函数
// 1、概述
//     1.1、函数的声明
//     （1）function 命令：function命令声明的代码区块，就是一个函数。function命令后面是函数名，函数名后面是一对圆括号，里面是传入函数的参数。函数体放在大括号里面。
//         function print(s) {
//         console.log(s);
//         }
//     （2）函数表达式：除了用function命令声明函数，还可以采用变量赋值的写法。 
//         var print = function(s) {
//           console.log(s);
//         };
//     （3）Function 构造函数：第三种声明函数的方式是Function构造函数。
//         var add = new Function(
//           'x',
//           'y',
//           'return x + y'
//         );
//         // 等同于
//         function add(x, y) {
//           return x + y;
//         }
//     1.2、函数的重复声明：如果同一个函数被多次声明，后面的声明就会覆盖前面的声明。   
//     1.3、圆括号运算符，return 语句和递归：调用函数时，要使用圆括号运算符。圆括号之中，可以加入函数的参数。
//     1.4、第一等公民：JavaScript 语言将函数看作一种值，与其它值（数值、字符串、布尔值等等）地位相同。凡是可以使用值的地方，就能使用函数。比如，可以把函数赋值给变量和对象的属性，
//     也可以当作参数传入其他函数，或者作为函数的结果返回。函数只是一个可以执行的值，此外并无特殊之处。
//     1.5、函数名的提升：JavaScript 引擎将函数名视同变量名，所以采用function命令声明函数时，整个函数会像变量声明一样，被提升到代码头部。所以，下面的代码不会报错。
// 2、函数的属性和方法
//     2.1、name 属性：函数的name属性返回函数的名字。
//     2.2、length 属性：函数的length属性返回函数预期传入的参数个数，即函数定义之中的参数个数。
//     2.3、toString()：函数的toString()方法返回一个字符串，内容是函数的源码。
// 3、函数作用域
//     3.1、定义：作用域（scope）指的是变量存在的范围。在 ES5 的规范中，JavaScript 只有两种作用域：一种是全局作用域，变量在整个程序中一直存在，所有地方都可以读取；
//         另一种是函数作用域，变量只在函数内部存在。ES6 又新增了块级作用域，本教程不涉及。
//         对于顶层函数来说，函数外部声明的变量就是全局变量（global variable），它可以在函数内部读取。
//     3.2、函数内部的变量提升：与全局作用域一样，函数作用域内部也会产生“变量提升”现象。var命令声明的变量，不管在什么位置，变量声明都会被提升到函数体的头部。
//     3.3、函数本身的作用域：函数本身也是一个值，也有自己的作用域。它的作用域与变量一样，就是其声明时所在的作用域，与其运行时所在的作用域无关。
// 4、参数
//     4.1、概述：函数运行的时候，有时需要提供外部数据，不同的外部数据会得到不同的结果，这种外部数据就叫参数。
//     4.2、参数的省略：函数参数不是必需的，JavaScript 允许省略参数。
//     4.3、传递方式：函数参数如果是原始类型的值（数值、字符串、布尔值），传递方式是传值传递（passes by value）。这意味着，在函数体内修改参数值，不会影响到函数外部。
//     4.4、同名参数：如果有同名的参数，则取最后出现的那个值。
//     4.5、arguments 对象
//     （1）定义
//         由于 JavaScript 允许函数有不定数目的参数，所以需要一种机制，可以在函数体内部读取所有参数。这就是arguments对象的由来。
//         arguments对象包含了函数运行时的所有参数，arguments[0]就是第一个参数，arguments[1]就是第二个参数，以此类推。这个对象只有在函数体内部，才可以使用。
//     （2）与数组的关系
//         需要注意的是，虽然arguments很像数组，但它是一个对象。数组专有的方法（比如slice和forEach），不能在arguments对象上直接使用。
//         如果要让arguments对象使用数组方法，真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法：slice方法和逐一填入新数组。
//     （3）callee 属性：arguments对象带有一个callee属性，返回它所对应的原函数。
// 5、函数的其他知识点
//     5.1、闭包：闭包（closure）是 JavaScript 语言的一个难点，也是它的特色，很多高级应用都要依靠闭包实现。
//         理解闭包，首先必须理解变量作用域。前面提到，JavaScript 有两种作用域：全局作用域和函数作用域。函数内部可以直接读取全局变量。
//         闭包的最大用处有两个，一个是可以读取外层函数内部的变量，另一个就是让这些变量始终保持在内存中，即闭包可以使得它诞生环境一直存在。
//         注意，外层函数每次运行，都会生成一个新的闭包，而这个闭包又会保留外层函数的内部变量，所以内存消耗很大。因此不能滥用闭包，否则会造成网页的性能问题。
//     5.2、立即调用的函数表达式（IIFE）
//         根据 JavaScript 的语法，圆括号()跟在函数名之后，表示调用该函数。比如，print()就表示调用print函数。
//         函数定义后立即调用的解决方法，就是不要让function出现在行首，让引擎将其理解成一个表达式。最简单的处理，就是将其放在一个圆括号里面。
//         (function(){ /* code */ }());
//         // 或者
//         (function(){ /* code */ })();
// 6、eval 命令
//     6.1、基本用法：eval命令接受一个字符串作为参数，并将这个字符串当作语句执行。
//     6.2、eval 的别名调用：前面说过eval不利于引擎优化执行速度。更麻烦的是，还有下面这种情况，引擎在静态代码分析的阶段，根本无法分辨执行的是eval。
///**ES6 函数*/
// 7、函数的扩展
//     7.1、函数参数的默认值：
//     （1）基本用法：ES6 允许为函数的参数设置默认值，即直接写在参数定义的后面。
//         function log(x, y = 'World') {
//             console.log(x, y);
//         }
//     （2）与解构赋值默认值结合使用
//         function fetch(url, { body = '', method = 'GET', headers = {} }) {
//             console.log(method);
//         }
//         fetch('http://example.com', {})
//         // "GET"
//         fetch('http://example.com')
//         // 报错
//     （3）参数默认值的位置：通常情况下，定义了默认值的参数，应该是函数的尾参数。因为这样比较容易看出来，到底省略了哪些参数。
//         如果非尾部的参数设置默认值，实际上这个参数是没法省略的。
//     （4）函数的 length 属性： 指定了默认值以后，函数的length属性，将返回没有指定默认值的参数个数。也就是说，指定了默认值后，length属性将失真。
//     （5）作用域：一旦设置了参数的默认值，函数进行声明初始化时，参数会形成一个单独的作用域（context）。
//         等到初始化结束，这个作用域就会消失。这种语法行为，在不设置参数默认值时，是不会出现的。
//     （6）应用：利用参数默认值，可以指定某一个参数不得省略，如果省略就抛出一个错误。
//     7.2、rest 参数：ES6 引入 rest 参数（形式为...变量名），用于获取函数的多余参数，这样就不需要使用arguments对象了。
//         rest 参数搭配的变量是一个数组，该变量将多余的参数放入数组中。
//     7.3、严格模式：
//         从 ES5 开始，函数内部可以设定为严格模式。
//         function doSomething(a, b) {
//         'use strict';
//         // code
//         }
//         ES2016 做了一点修改，规定只要函数参数使用了默认值、解构赋值、或者扩展运算符，那么函数内部就不能显式设定为严格模式，否则会报错。
//     7.4、name 属性：如果将一个匿名函数赋值给一个变量，ES5 的name属性，会返回空字符串，而 ES6 的name属性会返回实际的函数名。
//         var f = function () {};
//         // ES5
//         f.name // ""
//         // ES6
//         f.name // "f"
//     7.5、箭头函数
//     （1）基本用法：ES6 允许使用“箭头”（=>）定义函数。
//         var f = v => v;
//         // 等同于
//         var f = function (v) {
//         return v;
//         };
//     （2）使用注意点：箭头函数有几个使用注意点。
//         （a）函数体内的this对象，就是定义时所在的对象，而不是使用时所在的对象。
//         （b）不可以当作构造函数，也就是说，不可以使用new命令，否则会抛出一个错误。
//         （c）不可以使用arguments对象，该对象在函数体内不存在。如果要用，可以用 rest 参数代替。
//         （d）不可以使用yield命令，因此箭头函数不能用作 Generator 函数。
//         上面四点中，第一点尤其值得注意。this对象的指向是可变的，但是在箭头函数中，它是固定的。
//     (3)不适用场合
//         (a)第一个场合是定义对象的方法，且该方法内部包括this。
//             const cat = {
//                 lives: 9,
//                 jumps: () => {
//                     this.lives--;
//                 }
//             }
//         (b)第二个场合是需要动态this的时候，也不应使用箭头函数。
//             var button = document.getElementById('press');
//             button.addEventListener('click', () => {
//             this.classList.toggle('on');
//             });
//     （4）嵌套的箭头函数：箭头函数内部，还可以再使用箭头函数。
//     7.6、尾调用优化
//     （1）什么是尾调用？：尾调用（Tail Call）是函数式编程的一个重要概念，本身非常简单，一句话就能说清楚，就是指某个函数的最后一步是调用另一个函数。
//         以下三种情况，都不属于尾调用。
//         // 情况一
//         function f(x){
//         let y = g(x);
//         return y;
//         }
//         // 情况二
//         function f(x){
//         return g(x) + 1;
//         }
//         // 情况三
//         function f(x){
//         g(x);
//         }
//     （2）尾调用优化：如果所有函数都是尾调用，那么完全可以做到每次执行时，调用帧只有一项，这将大大节省内存。这就是“尾调用优化”的意义。
//         注意，只有不再用到外层函数的内部变量，内层函数的调用帧才会取代外层函数的调用帧，否则就无法进行“尾调用优化”。
//     （3）尾递归：函数调用自身，称为递归。如果尾调用自身，就称为尾递归。 
//         递归非常耗费内存，因为需要同时保存成千上百个调用帧，很容易发生“栈溢出”错误（stack overflow）。
//         但对于尾递归来说，由于只存在一个调用帧，所以永远不会发生“栈溢出”错误。
//         function factorial(n) {
//         if (n === 1) return 1;
//         return n * factorial(n - 1);
//         }
//         factorial(5) // 120
//     （4）递归函数的改写：尾递归的实现，往往需要改写递归函数，确保最后一步只调用自身。做到这一点的方法，就是把所有用到的内部变量改写成函数的参数。    
//     （5）严格模式：ES6 的尾调用优化只在严格模式下开启，正常模式是无效的。
//         这是因为在正常模式下，函数内部有两个变量，可以跟踪函数的调用栈。
//             func.arguments：返回调用时函数的参数。
//             func.caller：返回调用当前函数的那个函数。
//     （6）尾递归优化的实现
//         尾递归优化只在严格模式下生效，那么正常模式下，或者那些不支持该功能的环境中，有没有办法也使用尾递归优化呢？回答是可以的，就是自己实现尾递归优化。
//         它的原理非常简单。尾递归之所以需要优化，原因是调用栈太多，造成溢出，那么只要减少调用栈，就不会溢出。怎么做可以减少调用栈呢？就是采用“循环”换掉“递归”。
//     7.7、函数参数的尾逗号：ES2017 允许函数的最后一个参数有尾逗号（trailing comma）。
//         此前，函数定义和调用时，都不允许最后一个参数后面出现逗号。
//     7.8、Function.prototype.toString()：ES2019 对函数实例的toString()方法做出了修改。
//         toString()方法返回函数代码本身，以前会省略注释和空格。
//     7.9、catch 命令的参数省略：JavaScript 语言的try...catch结构，以前明确要求catch命令后面必须跟参数，接受try代码块抛出的错误对象。
//         try {
//             // ...
//         } catch {
//             // ...
//         }
      

// // 十三、Symbol
// 1、概述：
//     ES6 引入了一种新的原始数据类型Symbol，表示独一无二的值。它是 JavaScript 语言的第七种数据类型，
//         前六种是：undefined、null、布尔值（Boolean）、字符串（String）、数值（Number）、对象（Object）。
//     Symbol 值通过Symbol函数生成。这就是说，对象的属性名现在可以有两种类型，一种是原来就有的字符串，另一种就是新增的 Symbol 类型。
//         凡是属性名属于 Symbol 类型，就都是独一无二的，可以保证不会与其他属性名产生冲突。
// 2、Symbol.prototype.description ：创建 Symbol 的时候，可以添加一个描述。
// 3、作为属性名的 Symbol：由于每一个 Symbol 值都是不相等的，这意味着 Symbol 值可以作为标识符，用于对象的属性名，就能保证不会出现同名的属性。
//     这对于一个对象由多个模块构成的情况非常有用，能防止某一个键被不小心改写或覆盖。
// 4、实例：消除魔术字符串：魔术字符串指的是，在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码，应该尽量消除魔术字符串，改由含义清晰的变量代替。
//     function getArea(shape, options) {
//     let area = 0;

//     switch (shape) {
//         case 'Triangle': // 魔术字符串
//         area = .5 * options.width * options.height;
//         break;
//         /* ... more code ... */
//     }

//     return area;
//     }
//     getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
// 5、属性名的遍历
//     Symbol 作为属性名，遍历对象的时候，该属性不会出现在for...in、for...of循环中，也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
//     但是，它也不是私有属性，有一个Object.getOwnPropertySymbols()方法，可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组，成员是当前对象的所有用作属性名的 Symbol 值。
// 6、Symbol.for()，Symbol.keyFor()
//     有时，我们希望重新使用同一个 Symbol 值，Symbol.for()方法可以做到这一点。它接受一个字符串作为参数，然后搜索有没有以该参数作为名称的 Symbol 值。
//         如果有，就返回这个 Symbol 值，否则就新建一个以该字符串为名称的 Symbol 值，并将其注册到全局。
//     Symbol.for()与Symbol()这两种写法，都会生成新的 Symbol。它们的区别是，前者会被登记在全局环境中供搜索，后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值，而是会先检查
//         给定的key是否已经存在，如果不存在才会新建一个值。比如，如果你调用Symbol.for("cat")30 次，每次都会返回同一个 Symbol 值，但是调用Symbol("cat")30 次，会返回 30 个不同的 Symbol 值。
//     Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。
// 7、实例：模块的 Singleton 模式：Singleton 模式指的是调用一个类，任何时候返回的都是同一个实例。对于 Node 来说，模块文件可以看成是一个类。
// 8、内置的 Symbol 值：除了定义自己使用的 Symbol 值以外，ES6 还提供了 11 个内置的 Symbol 值，指向语言内部使用的方法。
//     8.1、Symbol.hasInstance ： 对象的Symbol.hasInstance属性，指向一个内部方法。当其他对象使用instanceof运算符，判断是否为该对象的实例时，会调用这个方法。比如，
//         foo instanceof Foo在语言内部，实际调用的是Foo[Symbol.hasInstance](foo)。
//     8.2、Symbol.isConcatSpreadable ：对象的Symbol.isConcatSpreadable属性等于一个布尔值，表示该对象用于Array.prototype.concat()时，是否可以展开。
//     8.3、Symbol.species ：对象的Symbol.species属性，指向一个构造函数。创建衍生对象时，会使用该属性。
//     8.4、Symbol.match ：对象的Symbol.match属性，指向一个函数。当执行str.match(myObject)时，如果该属性存在，会调用它，返回该方法的返回值。
//     8.5、Symbol.replace ：对象的Symbol.replace属性，指向一个方法，当该对象被String.prototype.replace方法调用时，会返回该方法的返回值。
//     8.6、Symbol.search ：对象的Symbol.search属性，指向一个方法，当该对象被String.prototype.search方法调用时，会返回该方法的返回值。
//     8.7、Symbol.split ：对象的Symbol.split属性，指向一个方法，当该对象被String.prototype.split方法调用时，会返回该方法的返回值。
//     8.8、Symbol.iterator ：对象的Symbol.iterator属性，指向该对象的默认遍历器方法。
//     8.9、Symbol.toPrimitive ：对象的Symbol.toPrimitive属性，指向一个方法。该对象被转为原始类型的值时，会调用这个方法，返回该对象对应的原始类型值。
//     Symbol.toPrimitive被调用时，会接受一个字符串参数，表示当前运算的模式，一共有三种模式。
//         Number：该场合需要转成数值
//         String：该场合需要转成字符串
//         Default：该场合可以转成数值，也可以转成字符串
//     8.10、Symbol.toStringTag ：对象的Symbol.toStringTag属性，指向一个方法。在该对象上面调用Object.prototype.toString方法时，如果这个属性存在，它的返回值会出现在toString方法返回的字符串之中，
//         表示对象的类型。也就是说，这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。
//         * ES6 新增内置对象的Symbol.toStringTag属性值如下。
//             JSON[Symbol.toStringTag]：'JSON'
//             Math[Symbol.toStringTag]：'Math'
//             Module 对象M[Symbol.toStringTag]：'Module'
//             ArrayBuffer.prototype[Symbol.toStringTag]：'ArrayBuffer'
//             DataView.prototype[Symbol.toStringTag]：'DataView'
//             Map.prototype[Symbol.toStringTag]：'Map'
//             Promise.prototype[Symbol.toStringTag]：'Promise'
//             Set.prototype[Symbol.toStringTag]：'Set'
//             %TypedArray%.prototype[Symbol.toStringTag]：'Uint8Array'等
//             WeakMap.prototype[Symbol.toStringTag]：'WeakMap'
//             WeakSet.prototype[Symbol.toStringTag]：'WeakSet'
//             %MapIteratorPrototype%[Symbol.toStringTag]：'Map Iterator'
//             %SetIteratorPrototype%[Symbol.toStringTag]：'Set Iterator'
//             %StringIteratorPrototype%[Symbol.toStringTag]：'String Iterator'
//             Symbol.prototype[Symbol.toStringTag]：'Symbol'
//             Generator.prototype[Symbol.toStringTag]：'Generator'
//             GeneratorFunction.prototype[Symbol.toStringTag]：'GeneratorFunction'
//     8.11、Symbol.unscopables ：对象的Symbol.unscopables属性，指向一个对象。该对象指定了使用with关键字时，哪些属性会被with环境排除。


// //十四、Set 和 Map 数据结构
// 1、Set
//     1.1、基本用法 ：ES6 提供了新的数据结构 Set。它类似于数组，但是成员的值都是唯一的，没有重复的值。
//         Set本身是一个构造函数，用来生成 Set 数据结构。
//     1.2、Set 实例的属性和方法
//         Set 结构的实例有以下属性。
//             Set.prototype.constructor ：构造函数，默认就是Set函数。
//             Set.prototype.size：返回Set实例的成员总数。
//         Set 实例的方法分为两大类：操作方法（用于操作数据）和遍历方法（用于遍历成员）。下面先介绍四个操作方法。
//             Set.prototype.add(value) ：添加某个值，返回 Set 结构本身。
//             Set.prototype.delete(value) ：删除某个值，返回一个布尔值，表示删除是否成功。
//             Set.prototype.has(value) ：返回一个布尔值，表示该值是否为Set的成员。
//             Set.prototype.clear() ：清除所有成员，没有返回值。
//     1.3、遍历操作
//         Set 结构的实例有四个遍历方法，可以用于遍历成员。
//             Set.prototype.keys()：返回键名的遍历器
//             Set.prototype.values()：返回键值的遍历器
//             Set.prototype.entries()：返回键值对的遍历器
//             Set.prototype.forEach()：使用回调函数遍历每个成员
//     （1）keys()，values()，entries()
//         keys方法、values方法、entries方法返回的都是遍历器对象（详见《Iterator 对象》一章）。由于 Set 结构没有键名，只有键值（或者说键名和键值是同一个值），
//         所以keys方法和values方法的行为完全一致。
//     （2）forEach() ：Set 结构的实例与数组一样，也拥有forEach方法，用于对每个成员执行某种操作，没有返回值。
//     （3）遍历的应用 ：扩展运算符（...）内部使用for...of循环，所以也可以用于 Set 结构。  
// 2、WeakSet
//     2.1、含义 ：WeakSet 结构与 Set 类似，也是不重复的值的集合。但是，它与 Set 有两个区别。
//     （1）WeakSet 的成员只能是对象，而不能是其他类型的值。
//     （2）WeakSet 中的对象都是弱引用，即垃圾回收机制不考虑 WeakSet 对该对象的引用，也就是说，如果其他对象都不再引用该对象，那么垃圾回收机制会自动回收该
//         对象所占用的内存，不考虑该对象还存在于 WeakSet 之中。
//     2.2、语法 ：WeakSet 是一个构造函数，可以使用new命令，创建 WeakSet 数据结构。
//     WeakSet 结构有以下三个方法。
//         WeakSet.prototype.add(value)：向 WeakSet 实例添加一个新成员。
//         WeakSet.prototype.delete(value)：清除 WeakSet 实例的指定成员。
//         WeakSet.prototype.has(value)：返回一个布尔值，表示某个值是否在 WeakSet 实例之中。
// 3、Map
//     3.1、含义和基本用法 ：JavaScript 的对象（Object），本质上是键值对的集合（Hash 结构），但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
//         为了解决这个问题，ES6 提供了 Map 数据结构。它类似于对象，也是键值对的集合，但是“键”的范围不限于字符串，各种类型的值（包括对象）都可以当作键。
//         也就是说，Object 结构提供了“字符串—值”的对应，Map 结构提供了“值—值”的对应，是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构，Map 比 Object 更合适。
//     3.2、实例的属性和操作方法 ：Map 结构的实例有以下属性和操作方法。
//     （1）size 属性 ：size属性返回 Map 结构的成员总数。
//     （2）Map.prototype.set(key, value) ：set方法设置键名key对应的键值为value，然后返回整个 Map 结构。如果key已经有值，则键值会被更新，否则就新生成该键。
//     （3）Map.prototype.get(key) ：get方法读取key对应的键值，如果找不到key，返回undefined。
//     （4）Map.prototype.has(key) ：has方法返回一个布尔值，表示某个键是否在当前 Map 对象之中。
//     （5）Map.prototype.delete(key) ：delete方法删除某个键，返回true。如果删除失败，返回false。
//     （6）Map.prototype.clear() ：clear方法清除所有成员，没有返回值。
//     3.3、遍历方法 ：Map 结构原生提供三个遍历器生成函数和一个遍历方法。
//         Map.prototype.keys()：返回键名的遍历器。
//         Map.prototype.values()：返回键值的遍历器。
//         Map.prototype.entries()：返回所有成员的遍历器。
//         Map.prototype.forEach()：遍历 Map 的所有成员。
//     3.4、与其他数据结构的互相转换
//     （1）Map 转为数组 ：前面已经提过，Map 转为数组最方便的方法，就是使用扩展运算符（...）。
//     （2）数组 转为 Map ：将数组传入 Map 构造函数，就可以转为 Map。
//     （3）Map 转为对象 ：如果所有 Map 的键都是字符串，它可以无损地转为对象。
//     （4）对象转为 Map ：对象转为 Map 可以通过Object.entries()。
//     （5）Map 转为 JSON ：Map 转为 JSON 要区分两种情况。一种情况是，Map 的键名都是字符串，这时可以选择转为对象 JSON。
//     （6）JSON 转为 Map ：JSON 转为 Map，正常情况下，所有键名都是字符串。
// 4、WeakMap
//     4.1、含义 ：WeakMap结构与Map结构类似，也是用于生成键值对的集合。
//     4.2、WeakMap 的语法 ： WeakMap 与 Map 在 API 上的区别主要是两个，一是没有遍历操作（即没有keys()、values()和entries()方法），也没有size属性。
//         因为没有办法列出所有键名，某个键名是否存在完全不可预测，跟垃圾回收机制是否运行相关。这一刻可以取到键名，下一刻垃圾回收机制突然运行了，这个键名就没了，
//         为了防止出现不确定性，就统一规定不能取到键名。二是无法清空，即不支持clear方法。因此，WeakMap只有四个方法可用：get()、set()、has()、delete()。
//     4.3、WeakMap 的用途 ：WeakMap 应用的典型场合就是 DOM 节点作为键名。


// // 十五、Proxy
// 1、概述 ：Proxy 用于修改某些操作的默认行为，等同于在语言层面做出修改，所以属于一种“元编程”（meta programming），即对编程语言进行编程。
//     Proxy 可以理解成，在目标对象之前架设一层“拦截”，外界对该对象的访问，都必须先通过这层拦截，因此提供了一种机制，可以对外界的访问进行过滤和改写。
//     Proxy 这个词的原意是代理，用在这里表示由它来“代理”某些操作，可以译为“代理器”。
// 2、Proxy 实例的方法
//     2.1、get() ：get方法用于拦截某个属性的读取操作，可以接受三个参数，依次为目标对象、属性名和 proxy 实例本身（严格地说，是操作行为所针对的对象），其中最后一个参数可选。
//     2.2、set() ：set方法用来拦截某个属性的赋值操作，可以接受四个参数，依次为目标对象、属性名、属性值和 Proxy 实例本身，其中最后一个参数可选。
//     2.3、apply() ：apply方法拦截函数的调用、call和apply操作。apply方法可以接受三个参数，分别是目标对象、目标对象的上下文对象（this）和目标对象的参数数组。
//     2.4、has() ：has()方法用来拦截HasProperty操作，即判断对象是否具有某个属性时，这个方法会生效。典型的操作就是in运算符。
//         has()方法可以接受两个参数，分别是目标对象、需查询的属性名。
//     2.5、construct() ：construct()方法用于拦截new命令，下面是拦截对象的写法。
//         construct()方法可以接受三个参数。
//             target：目标对象。
//             args：构造函数的参数数组。
//             newTarget：创造实例对象时，new命令作用的构造函数（下面例子的p）。
//     2.6、deleteProperty() ：deleteProperty方法用于拦截delete操作，如果这个方法抛出错误或者返回false，当前属性就无法被delete命令删除。
//     2.7、defineProperty() ：defineProperty()方法拦截了Object.defineProperty()操作。
//     2.8、getOwnPropertyDescriptor() ：getOwnPropertyDescriptor()方法拦截Object.getOwnPropertyDescriptor()，返回一个属性描述对象或者undefined。
//     2.9、getPrototypeOf() ：getPrototypeOf()方法主要用来拦截获取对象原型。具体来说，拦截下面这些操作。
//         Object.prototype.__proto__
//         Object.prototype.isPrototypeOf()
//         Object.getPrototypeOf()
//         Reflect.getPrototypeOf()
//         instanceof
//     2.10、isExtensible() ：isExtensible()方法拦截Object.isExtensible()操作。
//     2.11、ownKeys() ：ownKeys()方法用来拦截对象自身属性的读取操作。具体来说，拦截以下操作。
//         Object.getOwnPropertyNames()
//         Object.getOwnPropertySymbols()
//         Object.keys()
//         for...in 循环
//     2.12、preventExtensions() ：preventExtensions()方法拦截Object.preventExtensions()。该方法必须返回一个布尔值，否则会被自动转为布尔值。
//     2.13、setPrototypeOf() ：setPrototypeOf()方法主要用来拦截Object.setPrototypeOf()方法。
// 3、Proxy.revocable() ：Proxy.revocable()方法返回一个可取消的 Proxy 实例。    
// 4、this 问题 ：虽然 Proxy 可以代理针对目标对象的访问，但它不是目标对象的透明代理，即不做任何拦截的情况下，也无法保证与目标对象的行为一致。
//     主要原因就是在 Proxy 代理的情况下，目标对象内部的this关键字会指向 Proxy 代理。
// 5、实例：Web 服务的客户端 ：Proxy 对象可以拦截目标对象的任意属性，这使得它很合适用来写 Web 服务的客户端。
//     const service = createWebService('http://example.com/data');
//     service.employees().then(json => {
//     const employees = JSON.parse(json);
//     // ···
//     });
    

// 十六、Reflect
// 1、概述 ：Reflect对象与Proxy对象一样，也是 ES6 为了操作对象而提供的新 API。
//     1.1、将Object对象的一些明显属于语言内部的方法（比如Object.defineProperty），放到Reflect对象上。现阶段，某些方法同时在Object和Reflect对象上部署，
//         未来的新方法将只部署在Reflect对象上。也就是说，从Reflect对象上可以拿到语言内部的方法。
//     1.2、修改某些Object方法的返回结果，让其变得更合理。比如，Object.defineProperty(obj, name, desc)在无法定义属性时，会抛出一个错误，而 
//         Reflect.defineProperty(obj, name, desc)则会返回false。
//     1.3、让Object操作都变成函数行为。某些Object操作是命令式，比如name in obj和delete obj[name]，而Reflect.has(obj, name)和 
//         Reflect.deleteProperty(obj, name)让它们变成了函数行为。
//     1.4、Reflect对象的方法与Proxy对象的方法一一对应，只要是Proxy对象的方法，就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法，
//         完成默认行为，作为修改行为的基础。也就是说，不管Proxy怎么修改默认行为，你总可以在Reflect上获取默认行为。
// 2、静态方法 ：Reflect对象一共有 13 个静态方法。
//     2.1、Reflect.get(target, name, receiver) ：Reflect.get方法查找并返回target对象的name属性，如果没有该属性，则返回undefined。
//     2.2、Reflect.set(target, name, value, receiver) ：Reflect.set方法设置target对象的name属性等于value。
//     2.3、Reflect.has(obj, name) ：Reflect.has方法对应name in obj里面的in运算符。
//     2.4、Reflect.deleteProperty(obj, name) ：Reflect.deleteProperty方法等同于delete obj[name]，用于删除对象的属性。
//     2.5、Reflect.construct(target, args) ：Reflect.construct方法等同于new target(...args)，这提供了一种不使用new，来调用构造函数的方法。
//     2.6、Reflect.getPrototypeOf(obj) ：Reflect.getPrototypeOf方法用于读取对象的__proto__属性，对应Object.getPrototypeOf(obj)。
//     2.7、Reflect.setPrototypeOf(obj, newProto) ：Reflect.setPrototypeOf方法用于设置目标对象的原型（prototype），对应Object.setPrototypeOf(obj, newProto)方法。
//         它返回一个布尔值，表示是否设置成功。
//     2.8、Reflect.apply(func, thisArg, args) ：Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args)，用于绑定this对象后执行给定函数。
//     2.9、Reflect.defineProperty(target, propertyKey, attributes)
//         Reflect.defineProperty方法基本等同于Object.defineProperty，用来为对象定义属性。未来，后者会被逐渐废除，请从现在开始就使用Reflect.defineProperty代替它。
//     2.10、Reflect.getOwnPropertyDescriptor(target, propertyKey)
//         Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor，用于得到指定属性的描述对象，将来会替代掉后者。
//     2.11、Reflect.isExtensible (target) ：Reflect.isExtensible方法对应Object.isExtensible，返回一个布尔值，表示当前对象是否可扩展。
//     2.12、Reflect.preventExtensions(target)
//         Reflect.preventExtensions对应Object.preventExtensions方法，用于让一个对象变为不可扩展。它返回一个布尔值，表示是否操作成功。
//     2.13、Reflect.ownKeys (target)
//         Reflect.ownKeys方法用于返回对象的所有属性，基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
// 3、实例：使用 Proxy 实现观察者模式 ：观察者模式（Observer mode）指的是函数自动观察数据对象，一旦对象有变化，函数就会自动执行。
//     const person = observable({
//     name: '张三',
//     age: 20
//     });
//     function print() {
//     console.log(`${person.name}, ${person.age}`)
//     }
//     observe(print);
//     person.name = '李四';
//     // 输出
//     // 李四, 20


// // 十七、Promise 对象
// 1、Promise 的含义 ：Promise 是异步编程的一种解决方案，比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现，
//     ES6 将其写进了语言标准，统一了用法，原生提供了Promise对象。
//     1.1、对象的状态不受外界影响。Promise对象代表一个异步操作，有三种状态：pending（进行中）、fulfilled（已成功）和rejected（已失败）。
//         只有异步操作的结果，可以决定当前是哪一种状态，任何其他操作都无法改变这个状态。这也是Promise这个名字的由来，它的英语意思就是“承诺”，
//         表示其他手段无法改变。
//     1.2、一旦状态改变，就不会再变，任何时候都可以得到这个结果。Promise对象的状态改变，只有两种可能：从pending变为fulfilled和从pending变为rejected。
//         只要这两种情况发生，状态就凝固了，不会再变了，会一直保持这个结果，这时就称为 resolved（已定型）。如果改变已经发生了，你再对Promise对象添加回调函数，
//         也会立即得到这个结果。这与事件（Event）完全不同，事件的特点是，如果你错过了它，再去监听，是得不到结果的。
// 2、基本用法 ：ES6 规定，Promise对象是一个构造函数，用来生成Promise实例。
//     const promise = new Promise(function(resolve, reject) {
//         // ... some code
//         if (/* 异步操作成功 */){
//         resolve(value);
//         } else {
//         reject(error);
//         }
//     });
//     Promise 构造函数接受一个函数作为参数，该函数的两个参数分别是resolve和reject。它们是两个函数，由 JavaScript 引擎提供，不用自己部署。
//     resolve 函数的作用是，将Promise对象的状态从“未完成”变为“成功”（即从 pending 变为 resolved），在异步操作成功时调用，并将异步操作的结果，作为参数传递出去；
//         reject 数的作用是，将Promise对象的状态从“未完成”变为“失败”（即从 pending 变为 rejected），在异步操作失败时调用，并将异步操作报出的错误，作为参数传递出去。
//     Promise 实例生成以后，可以用then方法分别指定resolved状态和rejected状态的回调函数。
//     then 方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用，第二个回调函数是Promise对象的状态变为rejected时调用。
//         这两个函数都是可选的，不一定要提供。它们都接受Promise对象传出的值作为参数。
// 3、Promise.prototype.then() ：Promise 实例具有then方法，也就是说，then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。
//     前面说过，then 方法的第一个参数是resolved状态的回调函数，第二个参数是 rejected 状态的回调函数，它们都是可选的。
// 4、Promise.prototype.catch()
//     Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名，用于指定发生错误时的回调函数。
// 5、Promise.prototype.finally() ：finally()方法用于指定不管 Promise 对象最后状态如何，都会执行的操作。
// 6、Promise.all() ：Promise.all()方法用于将多个 Promise 实例，包装成一个新的 Promise 实例。
//     const p = Promise.all([p1, p2, p3]);
//     p的状态由p1、p2、p3决定，分成两种情况。
//     （1）只有p1、p2、p3的状态都变成fulfilled，p的状态才会变成fulfilled，此时p1、p2、p3的返回值组成一个数组，传递给p的回调函数。
//     （2）只要p1、p2、p3之中有一个被rejected，p的状态就变成rejected，此时第一个被reject的实例的返回值，会传递给p的回调函数。
// 7、Promise.race()
//     Promise.race()方法同样是将多个 Promise 实例，包装成一个新的 Promise 实例。
//     const p = Promise.race([p1, p2, p3]);
// 8、Promise.allSettled()
//     Promise.allSettled()方法接受一组 Promise 实例作为参数，包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果，不管是fulfilled还是rejected，包装实例才会结束。
// 9、Promise.any()
//     ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数，包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态，包装实例就会变成fulfilled状态；
//         如果所有参数实例都变成rejected状态，包装实例就会变成rejected状态。
//     Promise.any()跟Promise.race()方法很像，只有一点不同，就是不会因为某个 Promise 变成rejected状态而结束。
// 10、Promise.resolve() ：有时需要将现有对象转为 Promise 对象，Promise.resolve()方法就起到这个作用。
//     Promise.resolve()方法的参数分成四种情况。
//     （1）参数是一个 Promise 实例
//         如果参数是 Promise 实例，那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
//     （2）参数是一个thenable对象
//         thenable 对象指的是具有then方法的对象，比如下面这个对象。
//     （3）参数不是具有then()方法的对象，或根本就不是对象
//         如果参数是一个原始值，或者是一个不具有then()方法的对象，则Promise.resolve()方法返回一个新的 Promise 对象，状态为resolved。
//     （4）不带有任何参数
//         Promise.resolve()方法允许调用时不带参数，直接返回一个resolved状态的 Promise 对象。
// 11、Promise.reject() ：Promise.reject(reason)方法也会返回一个新的 Promise 实例，该实例的状态为rejected。
// 12、应用
//     12.1、加载图片 ：我们可以将图片的加载写成一个Promise，一旦加载完成，Promise的状态就发生变化。
//     12.2、Generator 函数与 Promise 的结合 ：使用 Generator 函数管理流程，遇到异步操作的时候，通常返回一个Promise对象。
// 13、Promise.try()
//     实际开发中，经常遇到一种情况：不知道或者不想区分，函数f是同步函数还是异步操作，但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作，都用then方法指定下一步流程，
//     用catch方法处理f抛出的错误。一般就会采用下面的写法。


// // 十八、Iterator 和 for...of 循环
// 1、Iterator（遍历器）的概念
//     JavaScript 原有的表示“集合”的数据结构，主要是数组（Array）和对象（Object），ES6 又添加了Map和Set。
//     遍历器（Iterator）就是这样一种机制。它是一种接口，为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口，就可以完成遍历操作（即依次处理该数据结构的所有成员）。
//     Iterator 的作用有三个：一是为各种数据结构，提供一个统一的、简便的访问接口；二是使得数据结构的成员能够按某种次序排列；三是 ES6 创造了一种新的遍历命令for...of循环，
//         Iterator 接口主要供for...of消费。
//     Iterator 的遍历过程是这样的。
//     （1）创建一个指针对象，指向当前数据结构的起始位置。也就是说，遍历器对象本质上，就是一个指针对象。    
//     （2）第一次调用指针对象的next方法，可以将指针指向数据结构的第一个成员。 
//     （3）第二次调用指针对象的next方法，指针就指向数据结构的第二个成员。   
//     （4）不断调用指针对象的next方法，直到它指向数据结构的结束位置。
// 2、默认 Iterator 接口 ：Iterator 接口的目的，就是为所有数据结构，提供了一种统一的访问机制，即for...of循环。当使用for...of循环遍历某种数据结构时，该循环会自动去寻找 Iterator 接口。
//     ES6 规定，默认的 Iterator 接口部署在数据结构的Symbol.iterator属性，或者说，一个数据结构只要具有Symbol.iterator属性，就可以认为是“可遍历的”（iterable）。
//     Symbol.iterator属性本身是一个函数，就是当前数据结构默认的遍历器生成函数。执行这个函数，就会返回一个遍历器。
//     原生具备 Iterator 接口的数据结构如下。
//         Array
//         Map
//         Set
//         String
//         TypedArray
//         函数的 arguments 对象
//         NodeList 对象
// 3、调用 Iterator 接口的场合 ：有一些场合会默认调用 Iterator 接口（即Symbol.iterator方法），除了下文会介绍的for...of循环，还有几个别的场合。
//     3.1、解构赋值 ：对数组和 Set 结构进行解构赋值时，会默认调用Symbol.iterator方法。
//     3.2、扩展运算符 ：扩展运算符（...）也会调用默认的 Iterator 接口。
//     3.3、yield* ：yield*后面跟的是一个可遍历的结构，它会调用该结构的遍历器接口。
//     3.4、其他场合 ：由于数组的遍历会调用遍历器接口，所以任何接受数组作为参数的场合，其实都调用了遍历器接口。下面是一些例子。
//         for...of
//         Array.from()
//         Map(), Set(), WeakMap(), WeakSet()（比如new Map([['a',1],['b',2]])）
//         Promise.all()
//         Promise.race()
// 4、字符串的 Iterator 接口 ：字符串是一个类似数组的对象，也原生具有 Iterator 接口。 
// 5、Iterator 接口与 Generator 函数
// 6、遍历器对象的 return()，throw()
//     遍历器对象除了具有next()方法，还可以具有return()方法和throw()方法。如果你自己写遍历器对象生成函数，那么next()方法是必须部署的，return()方法和throw()方法是否部署是可选的。
//     return()方法的使用场合是，如果for...of循环提前退出（通常是因为出错，或者有break语句），就会调用return()方法。如果一个对象在完成遍历前，需要清理或释放资源，就可以部署return()方法。
//     throw()方法主要是配合 Generator 函数使用，一般的遍历器对象用不到这个方法。
// 7、for...of 循环
//     一个数据结构只要部署了Symbol.iterator属性，就被视为具有 iterator 接口，就可以用for...of循环遍历它的成员。也就是说，for...of循环内部调用的是数据结构的Symbol.iterator方法。
//     7.1、数组 ：数组原生具备iterator接口（即默认部署了Symbol.iterator属性），for...of循环本质上就是调用这个接口产生的遍历器，
//     7.2、Set 和 Map 结构 ：Set 和 Map 结构也原生具有 Iterator 接口，可以直接使用for...of循环。
//     7.3、计算生成的数据结构 ：有些数据结构是在现有数据结构的基础上，计算生成的。比如，ES6 的数组、Set、Map 都部署了以下三个方法，调用后都返回遍历器对象。
//         entries() 返回一个遍历器对象，用来遍历[键名, 键值]组成的数组。对于数组，键名就是索引值；对于 Set，键名与键值相同。Map 结构的 Iterator 接口，默认就是调用entries方法。
//         keys() 返回一个遍历器对象，用来遍历所有的键名。
//         values() 返回一个遍历器对象，用来遍历所有的键值。
//     7.4、类似数组的对象 ：类似数组的对象包括好几类。下面是for...of循环用于 字符串、DOM NodeList 对象、arguments对象的例子。
//     7.5、对象 ：对于普通的对象，for...of结构不能直接使用，会报错，必须部署了 Iterator 接口后才能使用。但是，这样情况下，for...in循环依然可以用来遍历键名。
//     7.6、与其他遍历语法的比较
//     优点：有着同for...in一样的简洁语法，但是没有for...in那些缺点。
//         不同于forEach方法，它可以与break、continue和return配合使用。
//         提供了遍历所有数据结构的统一操作接口。


// // 十九、Generator 函数的语法
// 1、简介
//     1.1、基本概念
//         Generator 函数有多种理解角度。语法上，首先可以把它理解成，Generator 函数是一个状态机，封装了多个内部状态。
//         形式上，Generator 函数是一个普通函数，但是有两个特征。一是，function关键字与函数名之间有一个星号；二是，函数体内部使用yield表达式，
//             定义不同的内部状态（yield在英语里的意思就是“产出”）。
//         调用 Generator 函数，返回一个遍历器对象，代表 Generator 函数的内部指针。以后，每次调用遍历器对象的next方法，就会返回一个有着value和done两个属性的对象。
//             value属性表示当前的内部状态的值，是yield表达式后面那个表达式的值；done属性是一个布尔值，表示是否遍历结束。
//     1.2、yield 表达式
//         由于 Generator 函数返回的遍历器对象，只有调用next方法才会遍历下一个内部状态，所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
//         遍历器对象的next方法的运行逻辑如下。
//         （1）遇到yield表达式，就暂停执行后面的操作，并将紧跟在yield后面的那个表达式的值，作为返回的对象的value属性值。
//         （2）下一次调用next方法时，再继续往下执行，直到遇到下一个yield表达式。
//         （3）如果没有再遇到新的yield表达式，就一直运行到函数结束，直到return语句为止，并将return语句后面的表达式的值，作为返回的对象的value属性值。
//         （4）如果该函数没有return语句，则返回的对象的value属性值为undefined。
//     1.3、与 Iterator 接口的关系
//         上一章说过，任意一个对象的Symbol.iterator方法，等于该对象的遍历器生成函数，调用该函数会返回该对象的一个遍历器对象。
//         由于 Generator 函数就是遍历器生成函数，因此可以把 Generator 赋值给对象的Symbol.iterator属性，从而使得该对象具有 Iterator 接口。
// 2、next 方法的参数
//     yield表达式本身没有返回值，或者说总是返回undefined。next方法可以带一个参数，该参数就会被当作上一个yield表达式的返回值。
//     Generator 函数从暂停状态到恢复运行，它的上下文状态（context）是不变的。通过next方法的参数，就有办法在 Generator 函数开始运行之后，继续向函数体内部注入值。
//         也就是说，可以在 Generator 函数运行的不同阶段，从外部向内部注入不同的值，从而调整函数行为。
// 3、for...of 循环 ：for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象，且此时不再需要调用next方法。
// 4、Generator.prototype.throw() ：Generator 函数返回的遍历器对象，都有一个throw方法，可以在函数体外抛出错误，然后在 Generator 函数体内捕获。
// 5、Generator.prototype.return() ：Generator 函数返回的遍历器对象，还有一个return()方法，可以返回给定的值，并且终结遍历 Generator 函数。
// 6、next()、throw()、return() 的共同点
//     next()、throw()、return()这三个方法本质上是同一件事，可以放在一起理解。它们的作用都是让 Generator 函数恢复执行，并且使用不同的语句替换yield表达式。
//     next()是将yield表达式替换成一个值。
//     throw()是将yield表达式替换成一个throw语句。
//     return()是将yield表达式替换成一个return语句。
// 7、yield* 表达式 ：如果在 Generator 函数内部，调用另一个 Generator 函数。需要在前者的函数体内部，自己手动完成遍历。
// 8、作为对象属性的 Generator 函数 ：如果一个对象的属性是 Generator 函数，可以简写成下面的形式。
//     let obj = {
//         * myGeneratorMethod() {
//             ···
//         }
//     };
// 9、Generator 函数的this
//     Generator 函数总是返回一个遍历器，ES6 规定这个遍历器是 Generator 函数的实例，也继承了 Generator 函数的prototype对象上的方法。
// 10、含义
//     10.1、Generator 与状态机 ：Generator 是实现状态机的最佳结构。比如，下面的clock函数就是一个状态机。
//     10.2、Generator 与协程 ：协程（coroutine）是一种程序运行的方式，可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现，也可以用多线程实现。
//         前者是一种特殊的子例程，后者是一种特殊的线程。
//     （1）协程与子例程的差异
//         传统的“子例程”（subroutine）采用堆栈式“后进先出”的执行方式，只有当调用的子函数完全执行完毕，才会结束执行父函数。协程与其不同，多个线程（单线程情况下，
//             即多个函数）可以并行执行，但是只有一个线程（或函数）处于正在运行的状态，其他线程（或函数）都处于暂停态（suspended），线程（或函数）之间可以交换执行权。
//             也就是说，一个线程（或函数）执行到一半，可以暂停执行，将执行权交给另一个线程（或函数），等到稍后收回执行权的时候，再恢复执行。这种可以并行执行、
//             交换执行权的线程（或函数），就称为协程。
//         从实现上看，在内存中，子例程只使用一个栈（stack），而协程是同时存在多个栈，但只有一个栈是在运行状态，也就是说，协程是以多占用内存为代价，实现多任务的并行。
//     （2）协程与普通线程的差异
//         不难看出，协程适合用于多任务运行的环境。在这个意义上，它与普通的线程很相似，都有自己的执行上下文、可以分享全局变量。它们的不同之处在于，同一时间可以有多个线程处于运行状态，
//             但是运行的协程只能有一个，其他协程都处于暂停状态。此外，普通的线程是抢先式的，到底哪个线程优先得到资源，必须由运行环境决定，但是协程是合作式的，执行权由协程自己分配。
//     10.3、Generator 与上下文
//         JavaScript 代码运行时，会产生一个全局的上下文环境（context，又称运行环境），包含了当前所有的变量和对象。然后，执行函数（或块级代码）的时候，又会在当前上下文环境的上层，
//         产生一个函数运行的上下文，变成当前（active）的上下文，由此形成一个上下文环境的堆栈（context stack）。
// 11、应用 ：Generator 可以暂停函数执行，返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
//     11.1、异步操作的同步化表达
//         Generator 函数的暂停执行的效果，意味着可以把异步操作写在yield表达式里面，等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了，
//         因为异步操作的后续操作可以放在yield表达式下面，反正要等到调用next方法时再执行。所以，Generator 函数的一个重要实际意义就是用来处理异步操作，改写回调函数。
//     11.2、控制流管理 
//         Generator 函数可以进一步改善代码运行流程。
//         function* longRunningTask(value1) {
//             try {
//                 var value2 = yield step1(value1);
//                 var value3 = yield step2(value2);
//                 var value4 = yield step3(value3);
//                 var value5 = yield step4(value4);
//                 // Do something with value4
//             } catch (e) {
//                 // Handle any error from step1 through step4
//             }
//         }
//         然后，使用一个函数，按次序自动执行所有步骤。
//         scheduler(longRunningTask(initialValue));
//         function scheduler(task) {
//             var taskObj = task.next(task.value);
//             // 如果Generator函数未结束，就继续调用
//             if (!taskObj.done) {
//                 task.value = taskObj.value
//                 scheduler(task);
//             }
//         }
//     11.3、部署 Iterator 接口 ：利用 Generator 函数，可以在任意对象上部署 Iterator 接口。
//     11.4、作为数据结构
//         Generator 可以看作是数据结构，更确切地说，可以看作是一个数组结构，因为 Generator 函数可以返回一系列的值，这意味着它可以对任意表达式，提供类似数组的接口。
///**Generator 函数的异步应用 */
// 12、传统方法 ：ES6 诞生以前，异步编程的方法，大概有下面四种。
//         回调函数
//         事件监听
//         发布/订阅
//         Promise 对象
//     Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。
// 13、基本概念
//     13.1、异步 ：所谓"异步"，简单说就是一个任务不是连续完成的，可以理解成该任务被人为分成两段，先执行第一段，然后转而执行其他任务，等做好了准备，再回过头执行第二段。
//     13.2、回调函数
//         JavaScript 语言对异步编程的实现，就是回调函数。所谓回调函数，就是把任务的第二段单独写在一个函数里面，等到重新执行这个任务的时候，就直接调用这个函数。
//         回调函数的英语名字callback，直译过来就是"重新调用"。
//     13.3、Promise
//         回调函数本身并没有问题，它的问题出现在多个回调函数嵌套。假定读取A文件之后，再读取B文件，代码如下。
//         fs.readFile(fileA, 'utf-8', function (err, data) {
//         fs.readFile(fileB, 'utf-8', function (err, data) {
//             // ...
//         });
//         });
// 14、Generator 函数
//     14.1、协程
//         传统的编程语言，早有异步编程的解决方案（其实是多任务的解决方案）。其中有一种叫做"协程"（coroutine），意思是多个线程互相协作，完成异步任务。
//         协程有点像函数，又有点像线程。它的运行流程大致如下。
//             第一步，协程A开始执行。
//             第二步，协程A执行到一半，进入暂停，执行权转移到协程B。
//             第三步，（一段时间后）协程B交还执行权。
//             第四步，协程A恢复执行。
//         上面流程的协程A，就是异步任务，因为它分成两段（或多段）执行。
//     14.2、协程的 Generator 函数实现
//         Generator 函数是协程在 ES6 的实现，最大特点就是可以交出函数的执行权（即暂停执行）。
//         整个 Generator 函数就是一个封装的异步任务，或者说是异步任务的容器。异步操作需要暂停的地方，都用yield语句注明。
//     14.3、Generator 函数的数据交换和错误处理
//         Generator 函数可以暂停执行和恢复执行，这是它能封装异步任务的根本原因。除此之外，它还有两个特性，使它可以作为异步编程的完整解决方案：
//         函数体内外的数据交换和错误处理机制。
//     14.4、异步任务的封装
//         下面看看如何使用 Generator 函数，执行一个真实的异步任务。
//         var fetch = require('node-fetch');
//         function* gen(){
//         var url = 'https://api.github.com/users/github';
//         var result = yield fetch(url);
//         console.log(result.bio);
//         }
// 15、Thunk 函数 ：Thunk 函数是自动执行 Generator 函数的一种方法。
//     15.1、参数的求值策略 ：即函数的参数到底应该何时求值
//         var x = 1;
//         function f(m) {
//         return m * 2;
//         }
//         f(x + 5)
//         一种意见是"传值调用"（call by value），即在进入函数体之前，就计算x + 5的值（等于 6），再将这个值传入函数f。C 语言就采用这种策略。
//         另一种意见是“传名调用”（call by name），即直接将表达式x + 5传入函数体，只在用到它的时候求值。Haskell 语言采用这种策略。
//     15.2、Thunk 函数的含义
//         编译器的“传名调用”实现，往往是将参数放到一个临时函数之中，再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
//     15.3、JavaScript 语言的 Thunk 函数
//         JavaScript 语言是传值调用，它的 Thunk 函数含义有所不同。在 JavaScript 语言中，Thunk 函数替换的不是表达式，而是多参数函数，
//         将其替换成一个只接受回调函数作为参数的单参数函数。
//     15.4、Thunkify 模块 ：生产环境的转换器，建议使用 Thunkify 模块。
//         首先是安装。
//         $ npm install thunkify
//     15.5、Generator 函数的流程管理 ：但是 ES6 有了 Generator 函数，Thunk 函数现在可以用于 Generator 函数的自动流程管理。
//     15.6、Thunk 函数的自动流程管理
//         Thunk 函数真正的威力，在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
// 16、co 模块
//     16.1、基本用法 ：co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具，用于 Generator 函数的自动执行。
//     16.2、co 模块的原理
//         前面说过，Generator 就是一个异步操作的容器。它的自动执行需要一种机制，当异步操作有了结果，能够自动交回执行权。
//         两种方法可以做到这一点。
//         （1）回调函数。将异步操作包装成 Thunk 函数，在回调函数里面交回执行权。
//         （2）Promise 对象。将异步操作包装成 Promise 对象，用then方法交回执行权。
//     16.3、基于 Promise 对象的自动执行
//     16.4、co 模块的源码
//     16.5、处理并发的异步操作 ：co 支持并发的异步操作，即允许某些操作同时进行，等到它们全部完成，才进行下一步。
//     16.6、实例：处理 Stream
//         Node 提供 Stream 模式读写数据，特点是一次只处理数据的一部分，数据分成一块块依次处理，就好像“数据流”一样。这对于处理大规模数据非常有利。
//         Stream 模式使用 EventEmitter API，会释放三个事件。
//             data事件：下一块数据块已经准备好了。
//             end事件：整个“数据流”处理完了。
//             error事件：发生错误。
    

// // 二十、async 函数
// 1、含义
//     ES2017 标准引入了 async 函数，使得异步操作变得更加方便。
//     async 函数是什么？一句话，它就是 Generator 函数的语法糖。
//     async函数对 Generator 函数的改进，体现在以下四点。
//     （1）内置执行器。
//         Generator 函数的执行必须靠执行器，所以才有了co模块，而async函数自带执行器。也就是说，async函数的执行，与普通函数一模一样，只要一行。
//     （2）更好的语义。
//         async和await，比起星号和yield，语义更清楚了。async表示函数里有异步操作，await表示紧跟在后面的表达式需要等待结果。
//     （3）更广的适用性。
//         co模块约定，yield命令后面只能是 Thunk 函数或 Promise 对象，而async函数的await命令后面，可以是 Promise 对象和原始类型的值（数值、
//         字符串和布尔值，但这时会自动转成立即 resolved 的 Promise 对象）。
//     （4）返回值是 Promise。
//         async函数的返回值是 Promise 对象，这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
// 2、基本用法
//     async函数返回一个 Promise 对象，可以使用then方法添加回调函数。当函数执行的时候，一旦遇到await就会先返回，等到异步操作完成，
//     再接着执行函数体内后面的语句。
// 3、语法 ：async函数的语法规则总体上比较简单，难点是错误处理机制。
//     3.1、返回 Promise 对象 ：async函数返回一个 Promise 对象。
//         async函数内部return语句返回的值，会成为then方法回调函数的参数。
//     3.2、Promise 对象的状态变化
//         async函数返回的 Promise 对象，必须等到内部所有await命令后面的 Promise 对象执行完，才会发生状态改变，除非遇到return语句或者抛出错误。
//         也就是说，只有async函数内部的异步操作执行完，才会执行then方法指定的回调函数。
//     3.3、await 命令
//         正常情况下，await命令后面是一个 Promise 对象，返回该对象的结果。如果不是 Promise 对象，就直接返回对应的值。
//     3.4、错误处理
//         如果await后面的异步操作出错，那么等同于async函数返回的 Promise 对象被reject。
//     3.5、使用注意点
//         第一点，前面已经说过，await命令后面的Promise对象，运行结果可能是rejected，所以最好把await命令放在try...catch代码块中。
//         第二点，多个await命令后面的异步操作，如果不存在继发关系，最好让它们同时触发。
//         第三点，await命令只能用在async函数之中，如果用在普通函数，就会报错。
// 4、async 函数的实现原理
//     async 函数的实现原理，就是将 Generator 函数和自动执行器，包装在一个函数里。
// 5、与其他异步处理方法的比较
//     我们通过一个例子，来看 async 函数与 Promise、Generator 函数的比较。
//     假定某个 DOM 元素上面，部署了一系列的动画，前一个动画结束，才能开始后一个。如果当中有一个动画出错，就不再往下执行，返回上一个成功执行的动画的返回值。
// 6、实例：按顺序完成异步操作
// 7、顶层 await
//     根据语法规格，await命令只能出现在 async 函数内部，否则都会报错。

// // 二十一、Class 的基本语法
// 1、简介
//     1.1、类的由来
//         JavaScript 语言中，生成实例对象的传统方法是通过构造函数。
//         ES6 提供了更接近传统语言的写法，引入了 Class（类）这个概念，作为对象的模板。通过class关键字，可以定义类。
//     1.2、constructor 方法
//         constructor()方法是类的默认方法，通过new命令生成对象实例时，自动调用该方法。一个类必须有constructor()方法，
//         如果没有显式定义，一个空的constructor()方法会被默认添加。
//     1.3、类的实例
//         生成类的实例的写法，与 ES5 完全一样，也是使用new命令。前面说过，如果忘记加上new，像函数那样调用Class，将会报错。
//         这也意味着，可以通过实例的__proto__属性为“类”添加方法。
//             __proto__ 并不是语言本身的特性，这是各大厂商具体实现时添加的私有属性，虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性，
//             但依旧不建议在生产中使用该属性，避免对环境产生依赖。生产环境中，我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型，
//             然后再来为原型添加方法/属性。
//     1.4、取值函数（getter）和存值函数（setter）
//         与 ES5 一样，在“类”的内部可以使用get和set关键字，对某个属性设置存值函数和取值函数，拦截该属性的存取行为。
//     1.5、属性表达式 ：类的属性名，可以采用表达式。
//     1.6、Class 表达式 ：与函数一样，类也可以使用表达式的形式定义。   
//         const MyClass = class Me {
//             getClassName() {
//                 return Me.name;
//             }
//         };
//     1.7、注意点
//     （1）严格模式    
//         类和模块的内部，默认就是严格模式，所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中，就只有严格模式可用。考虑到未来所有的代码，
//         其实都是运行在模块之中，所以 ES6 实际上把整个语言升级到了严格模式。    
//     （2）不存在提升
//         类不存在变量提升（hoist），这一点与 ES5 完全不同。
//     （3）name 属性
//         由于本质上，ES6 的类只是 ES5 的构造函数的一层包装，所以函数的许多特性都被Class继承，包括name属性。
//     （4）Generator 方法
//         如果某个方法之前加上星号（*），就表示该方法是一个 Generator 函数。
//     （5）this 的指向
//         类的方法内部如果含有this，它默认指向类的实例。但是，必须非常小心，一旦单独使用该方法，很可能报错。
// 2、静态方法
//     类相当于实例的原型，所有在类中定义的方法，都会被实例继承。如果在一个方法前，加上 static 关键字，就表示该方法不会被实例继承，而是直接通过类来调用，这就称为“静态方法”。        
// 3、实例属性的新写法 ：实例属性除了定义在 constructor()方法里面的this上面，也可以定义在类的最顶层。
// 4、静态属性 ：静态属性指的是 Class 本身的属性，即Class.propName，而不是定义在实例对象（this）上的属性。
// 5、私有方法和私有属性
//     5.1、现有的解决方案
//         私有方法和私有属性，是只能在类的内部访问的方法和属性，外部不能访问。这是常见需求，有利于代码的封装，但 ES6 不提供，只能通过变通方法模拟实现。
//         一种做法是在命名上加以区别。
//         另一种方法就是索性将私有方法移出类，因为类内部的所有方法都是对外可见的。
//     5.2、私有属性的提案
//         目前，有一个提案，为class加了私有属性。方法是在属性名之前，使用#表示。
// 6、new.target 属性
//     new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性，该属性一般用在构造函数之中，返回new命令作用于的那个构造函数。
//     如果构造函数不是通过new命令或Reflect.construct()调用的，new.target会返回undefined，因此这个属性可以用来确定构造函数是怎么调用的。
///** Class 的继承 */
// 7、简介 ：Class 可以通过extends关键字实现继承，这比 ES5 的通过修改原型链实现继承，要清晰和方便很多。
// 8、Object.getPrototypeOf()
//     Object.getPrototypeOf方法可以用来从子类上获取父类。
// 9、super 关键字
//     super这个关键字，既可以当作函数使用，也可以当作对象使用。在这两种情况下，它的用法完全不同。
//     第一种情况，super作为函数调用时，代表父类的构造函数。ES6 要求，子类的构造函数必须执行一次super函数。
//     第二种情况，super作为对象时，在普通方法中，指向父类的原型对象；在静态方法中，指向父类。
// 10、类的 prototype 属性和__proto__属性
//     大多数浏览器的 ES5 实现之中，每一个对象都有__proto__属性，指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖，同时有prototype属性和
//         __proto__属性，因此同时存在两条继承链。
//     （1）子类的__proto__属性，表示构造函数的继承，总是指向父类。
//     （2）子类prototype属性的__proto__属性，表示方法的继承，总是指向父类的prototype属性。
//     10.1、实例的 __proto__ 属性
//         子类实例的__proto__属性的__proto__属性，指向父类实例的__proto__属性。也就是说，子类的原型的原型，是父类的原型。
// 11、原生构造函数的继承
//     原生构造函数是指语言内置的构造函数，通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
//         Boolean()
//         Number()
//         String()
//         Array()
//         Date()
//         Function()
//         RegExp()
//         Error()
//         Object()
// 12、Mixin 模式的实现
//     Mixin 指的是多个对象合成一个新的对象，新对象具有各个组成成员的接口。它的最简单实现如下。   
//     const a = {
//         a: 'a'
//       };
//       const b = {
//         b: 'b'
//       };
//       const c = {...a, ...b}; // {a: 'a', b: 'b'}


// // 二十二、Module 的语法
// 1、概述
//     历史上，JavaScript 一直没有模块（module）体系，无法将一个大程序拆分成互相依赖的小文件，再用简单的方法拼装起来。
//     由于 ES6 模块是编译时加载，使得静态分析成为可能。有了它，就能进一步拓宽 JavaScript 的语法，比如引入宏（macro）和类型检验（type system）这些只能靠静态分析实现的功能。
//     除了静态加载带来的各种好处，ES6 模块还有以下好处。
//         不再需要UMD模块格式了，将来服务器和浏览器都会支持 ES6 模块格式。目前，通过各种工具库，其实已经做到了这一点。
//         将来浏览器的新 API 就能用模块格式提供，不再必须做成全局变量或者navigator对象的属性。
//         不再需要对象作为命名空间（比如Math对象），未来这些功能可以通过模块提供。
// 2、严格模式
//     ES6 的模块自动采用严格模式，不管你有没有在模块头部加上"use strict";。
//     严格模式主要有以下限制。
//         变量必须声明后再使用
//         函数的参数不能有同名属性，否则报错
//         不能使用with语句
//         不能对只读属性赋值，否则报错
//         不能使用前缀 0 表示八进制数，否则报错
//         不能删除不可删除的属性，否则报错
//         不能删除变量delete prop，会报错，只能删除属性delete global[prop]
//         eval不会在它的外层作用域引入变量
//         eval和arguments不能被重新赋值
//         arguments不会自动反映函数参数的变化
//         不能使用arguments.callee
//         不能使用arguments.caller
//         禁止this指向全局对象
//         不能使用fn.caller和fn.arguments获取函数调用的堆栈
//         增加了保留字（比如protected、static和interface）
// 3、export 命令
//     模块功能主要由两个命令构成：export 和 import 。export 命令用于规定模块的对外接口，import 命令用于输入其他模块提供的功能。
//     一个模块就是一个独立的文件。该文件内部的所有变量，外部无法获取。如果你希望外部能够读取模块内部的某个变量，就必须使用 export 关键字输出该变量。
//     下面是一个 JS 文件，里面使用export命令输出变量。
// 4、import 命令
//     使用export命令定义了模块的对外接口以后，其他 JS 文件就可以通过 import 命令加载这个模块。
// 5、模块的整体加载
//     除了指定加载某个输出值，还可以使用整体加载，即用星号（*）指定一个对象，所有输出值都加载在这个对象上面。
// 6、export default 命令
//     为了给用户提供方便，让他们不用阅读文档就能加载模块，就要用到export default命令，为模块指定默认输出。
//     export default 命令用于指定模块的默认输出。
// 7、export 与 import 的复合写法
//     如果在一个模块之中，先输入后输出同一个模块，import语句可以与export语句写在一起。
//     export { foo, bar } from 'my_module';
//     // 可以简单理解为
//     import { foo, bar } from 'my_module';
//     export { foo, bar };
// 8、模块的继承 ：模块之间也可以继承。
// 9、跨模块常量 ：const声明的常量只在当前代码块有效
// 10、import()
//     10.1、简介
//         import 命令会被 JavaScript 引擎静态分析，先于模块内的其他语句执行（import命令叫做“连接” binding 其实更合适）。
//         ES2020提案 引入import()函数，支持动态加载模块。
//             import(specifier)
//             上面代码中，import函数的参数specifier，指定所要加载的模块的位置。import命令能够接受什么参数，import()
//             函数就能接受什么参数，两者区别主要是后者为动态加载。
//     10.2、适用场合
//     （1）按需加载 ：import()可以在需要的时候，再加载某个模块。
//     （2）条件加载 ：import()可以放在if代码块，根据不同的情况，加载不同的模块。
//     （3）动态的模块路径 ：import()允许模块路径动态生成。
//     10.3、注意点
//         import()加载模块成功以后，这个模块会作为一个对象，当作then方法的参数。因此，可以使用对象解构赋值的语法，获取输出接口。
//             import('./myModule.js')
//             .then(({export1, export2}) => {
//             // ...·
//             });
//         如果模块有default输出接口，可以用参数直接获得。
//             import('./myModule.js')
//             .then(myModule => {
//               console.log(myModule.default);
//             });
//         如果想同时加载多个模块，可以采用下面的写法。
//             Promise.all([
//               import('./module1.js'),
//               import('./module2.js'),
//               import('./module3.js'),
//             ])
//             .then(([module1, module2, module3]) => {
//                ···
//             });    
//         import()也可以用在 async 函数之中。
//             async function main() {
//                 const myModule = await import('./myModule.js');
//                 const {export1, export2} = await import('./myModule.js');
//                 const [module1, module2, module3] =
//                 await Promise.all([
//                     import('./module1.js'),
//                     import('./module2.js'),
//                     import('./module3.js'),
//                 ]);
//             }
//             main();
///** Module 的加载实现 */
// 11、浏览器加载
//     11.1、传统方法 ：HTML 网页中，浏览器通过<script>标签加载 JavaScript 脚本。
//         <script src="path/to/myModule.js" defer></script>
//         <script src="path/to/myModule.js" async></script>
//         defer与async的区别是：defer要等到整个页面在内存中正常渲染结束（DOM 结构完全生成，以及其他脚本执行完成），才会执行；async一旦下载完，渲染引擎就会中断渲染，
//         执行这个脚本以后，再继续渲染。一句话，defer是“渲染完再执行”，async是“下载完就执行”。另外，如果有多个defer脚本，会按照它们在页面出现的顺序加载，
//         而多个async脚本是不能保证加载顺序的。
//     11.2、加载规则
//         浏览器加载 ES6 模块，也使用<script>标签，但是要加入type="module"属性。
//         浏览器对于带有type="module"的<script>，都是异步加载，不会造成堵塞浏览器，即等到整个页面渲染完，再执行模块脚本，等同于打开了<script>标签的defer属性。
//         对于外部的模块脚本（ *.js），有几点需要注意。
//             代码是在模块作用域之中运行，而不是在全局作用域运行。模块内部的顶层变量，外部不可见。
//             模块脚本自动采用严格模式，不管有没有声明use strict。
//             模块之中，可以使用import命令加载其他模块（.js后缀不可省略，需要提供绝对 URL 或相对 URL），也可以使用export命令输出对外接口。
//             模块之中，顶层的this关键字返回undefined，而不是指向window。也就是说，在模块顶层使用this关键字，是无意义的。
//             同一个模块如果加载多次，将只执行一次。
// 12、ES6 模块与 CommonJS 模块的差异
//     讨论 Node.js 加载 ES6 模块之前，必须了解 ES6 模块与 CommonJS 模块完全不同。
//     它们有三个重大差异。
//         CommonJS 模块输出的是一个值的拷贝，ES6 模块输出的是值的引用。
//         CommonJS 模块是运行时加载，ES6 模块是编译时输出接口。
//         CommonJS 模块的require()是同步加载模块，ES6 模块的import命令是异步加载，有一个独立的模块依赖的解析阶段。
// 13、Node.js 的模块加载方法
//     13.1、概述
//         JavaScript 现在有两种模块。一种是 ES6 模块，简称 ESM；另一种是 CommonJS 模块，简称 CJS。
//         CommonJS 模块是 Node.js 专用的，与 ES6 模块不兼容。语法上面，两者最明显的差异是，CommonJS 模块使用require()和module.exports，ES6 模块使用import和export。
//     13.2、package.json 的 main 字段
//         package.json文件有两个字段可以指定模块的入口文件：main和exports。比较简单的模块，可以只使用main字段，指定模块加载的入口文件。
//     13.3、package.json 的 exports 字段
//         exports字段的优先级高于main字段。它有多种用法。
//         （1）子目录别名 ：package.json 文件的 exports 字段可以指定脚本或子目录的别名。
//         （2）main 的别名 ：exports 字段的别名如果是.，就代表模块的主入口，优先级高于 main 字段，并且可以直接简写成 exports 字段的值。
//         （3）条件加载
//             利用.这个别名，可以为 ES6 模块和 CommonJS 指定不同的入口。目前，这个功能需要在 Node.js 运行的时候，打开--experimental-conditional-exports标志。
//             {
//             "type": "module",
//             "exports": {
//                 ".": {
//                 "require": "./main.cjs",
//                 "default": "./main.js"
//                 }
//             }
//             }
//     13.4、CommonJS 模块加载 ES6 模块
//         CommonJS 的require()命令不能加载 ES6 模块，会报错，只能使用import()这个方法加载。
//         (async () => {
//         await import('./my-app.mjs');
//         })();
//         require()不支持 ES6 模块的一个原因是，它是同步加载，而 ES6 模块内部可以使用顶层await命令，导致无法被同步加载。
//     13.5、ES6 模块加载 CommonJS 模块
//         ES6 模块的import命令可以加载 CommonJS 模块，但是只能整体加载，不能只加载单一的输出项。
//     13.6、同时支持两种格式的模块
//         一个模块同时要支持 CommonJS 和 ES6 两种格式，也很容易。
//         如果原始模块是 ES6 格式，那么需要给出一个整体输出接口，比如export default obj，使得 CommonJS 可以用import()进行加载。
//     13.7、Node.js 的内置模块
//         Node.js 的内置模块可以整体加载，也可以加载指定的输出项。
//         // 整体加载
//         import EventEmitter from 'events';
//         const e = new EventEmitter();
//         // 加载指定的输出项
//         import { readFile } from 'fs';
//         readFile('./foo.txt', (err, source) => {
//         if (err) {
//             console.error(err);
//         } else {
//             console.log(source);
//         }
//         });
//     13.8、加载路径
//         ES6 模块的加载路径必须给出脚本的完整路径，不能省略脚本的后缀名。import命令和package.json文件的main字段如果省略脚本的后缀名，会报错。
//         // ES6 模块中将报错
//         import { something } from './index';
//         为了与浏览器的import加载规则相同，Node.js 的.mjs文件支持 URL 路径。
//         import './foo.mjs?query=1'; // 加载 ./foo 传入参数 ?query=1
//     13.9、内部变量
//         ES6 模块应该是通用的，同一个模块不用修改，就可以用在浏览器环境和服务器环境。为了达到这个目标，Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。
//         首先，就是this关键字。ES6 模块之中，顶层的this指向undefined；CommonJS 模块的顶层this指向当前模块，这是两者的一个重大差异。
//         其，以下这些顶层变量在 ES6 模块之中都是不存在的。
//             arguments
//             require
//             module
//             exports
//             __filename
//             __dirname
// 14、循环加载
//     “循环加载”（circular dependency）指的是，a脚本的执行依赖b脚本，而b脚本的执行又依赖a脚本。
//     14.1、CommonJS 模块的加载原理 
//         CommonJS 的一个模块，就是一个脚本文件。require命令第一次加载该脚本，就会执行整个脚本，然后在内存生成一个对象。
//     14.2、CommonJS 模块的循环加载
//         CommonJS 模块的重要特性是加载时执行，即脚本代码在require的时候，就会全部执行。一旦出现某个模块被"循环加载"，就只输出已经执行的部分，还未执行的部分不会输出。
//     14.3、ES6 模块的循环加载
//         ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用，如果使用import从一个模块加载变量（即import foo from 'foo'），那些变量不会被缓存，
//         而是成为一个指向被加载模块的引用，需要开发者自己保证，真正取值的时候能够取到值。


// 二十三、ArrayBuffer
// ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。
// 1、ArrayBuffer 对象
//     1.1、概述
//         ArrayBuffer对象代表储存二进制数据的一段内存，它不能直接读写，只能通过视图（TypedArray视图和DataView视图)来读写，视图的作用是以指定格式解读二进制数据。
//         ArrayBuffer也是一个构造函数，可以分配一段可以存放数据的连续内存区域。
//         const buf = new ArrayBuffer(32);
//     1.2、ArrayBuffer.prototype.byteLength
//         ArrayBuffer实例的byteLength属性，返回所分配的内存区域的字节长度。
//     1.3、ArrayBuffer.prototype.slice()
//         ArrayBuffer实例有一个slice方法，允许将内存区域的一部分，拷贝生成一个新的ArrayBuffer对象。
//     1.4、ArrayBuffer.isView()
//         ArrayBuffer有一个静态方法isView，返回一个布尔值，表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数，是否为TypedArray实例或DataView实例。
// 2、TypedArray 视图
//     2.1、概述
//         ArrayBuffer对象作为内存区域，可以存放多种类型的数据。同一段内存，不同数据有不同的解读方式，这就叫做“视图”（view）。ArrayBuffer有两种视图，一种是TypedArray视图，
//             另一种是DataView视图。前者的数组成员都是同一个数据类型，后者的数组成员可以是不同的数据类型。
//         目前，TypedArray视图一共包括 9 种类型，每一种视图都是一种构造函数。
//             Int8Array：8 位有符号整数，长度 1 个字节。
//             Uint8Array：8 位无符号整数，长度 1 个字节。
//             Uint8ClampedArray：8 位无符号整数，长度 1 个字节，溢出处理不同。
//             Int16Array：16 位有符号整数，长度 2 个字节。
//             Uint16Array：16 位无符号整数，长度 2 个字节。
//             Int32Array：32 位有符号整数，长度 4 个字节。
//             Uint32Array：32 位无符号整数，长度 4 个字节。
//             Float32Array：32 位浮点数，长度 4 个字节。
//             Float64Array：64 位浮点数，长度 8 个字节。
//         这 9 个构造函数生成的数组，统称为TypedArray视图。它们很像普通数组，都有length属性，都能用方括号运算符（[]）获取单个元素，所有数组的方法，在它们上面都能使用。
//             普通数组与 TypedArray 数组的差异主要在以下方面。
//             TypedArray 数组的所有成员，都是同一种类型。
//             TypedArray 数组的成员是连续的，不会有空位。
//             TypedArray 数组成员的默认值为 0。比如，new Array(10)返回一个普通数组，里面没有任何成员，只是 10 个空位；new Uint8Array(10)返回一个 TypedArray 数组，里面 10 个成员都是 0。
//             TypedArray 数组只是一层视图，本身不储存数据，它的数据都储存在底层的ArrayBuffer对象之中，要获取底层对象必须使用buffer属性。
//     2.2、构造函数 ：TypedArray 数组提供 9 种构造函数，用来生成相应类型的数组实例。
//     （1）TypedArray(buffer, byteOffset=0, length?)
//         同一个ArrayBuffer对象之上，可以根据不同的数据类型，建立多个视图。
//         视图的构造函数可以接受三个参数：
//             第一个参数（必需）：视图对应的底层ArrayBuffer对象。
//             第二个参数（可选）：视图开始的字节序号，默认从 0 开始。
//             第三个参数（可选）：视图包含的数据个数，默认直到本段内存区域结束。
//     （2）TypedArray(length)
//         视图还可以不通过ArrayBuffer对象，直接分配内存而生成。
//         const f64a = new Float64Array(8);
//     （3）TypedArray(typedArray)
//         TypedArray 数组的构造函数，可以接受另一个TypedArray实例作为参数。
//     （4）TypedArray(arrayLikeObject)
//         构造函数的参数也可以是一个普通数组，然后直接生成TypedArray实例。
//     2.3、数组方法 ：普通数组的操作方法和属性，对 TypedArray 数组完全适用。
//         TypedArray.prototype.copyWithin(target, start[, end = this.length])
//         TypedArray.prototype.entries()
//         TypedArray.prototype.every(callbackfn, thisArg?)
//         TypedArray.prototype.fill(value, start=0, end=this.length)
//         TypedArray.prototype.filter(callbackfn, thisArg?)
//         TypedArray.prototype.find(predicate, thisArg?)
//         TypedArray.prototype.findIndex(predicate, thisArg?)
//         TypedArray.prototype.forEach(callbackfn, thisArg?)
//         TypedArray.prototype.indexOf(searchElement, fromIndex=0)
//         TypedArray.prototype.join(separator)
//         TypedArray.prototype.keys()
//         TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
//         TypedArray.prototype.map(callbackfn, thisArg?)
//         TypedArray.prototype.reduce(callbackfn, initialValue?)
//         TypedArray.prototype.reduceRight(callbackfn, initialValue?)
//         TypedArray.prototype.reverse()
//         TypedArray.prototype.slice(start=0, end=this.length)
//         TypedArray.prototype.some(callbackfn, thisArg?)
//         TypedArray.prototype.sort(comparefn)
//         TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
//         TypedArray.prototype.toString()
//         TypedArray.prototype.values()
//     2.4、字节序 ：字节序指的是数值在内存中的表示方式。
//         const buffer = new ArrayBuffer(16);
//         const int32View = new Int32Array(buffer);
//         for (let i = 0; i < int32View.length; i++) {
//             int32View[i] = i * 2;
//         }
//     2.5、BYTES_PER_ELEMENT 属性 
//         每一种视图的构造函数，都有一个 BYTES_PER_ELEMENT 属性，表示这种数据类型占据的字节数。
//             Int8Array.BYTES_PER_ELEMENT // 1
//             Uint8Array.BYTES_PER_ELEMENT // 1
//             Uint8ClampedArray.BYTES_PER_ELEMENT // 1
//             Int16Array.BYTES_PER_ELEMENT // 2
//             Uint16Array.BYTES_PER_ELEMENT // 2
//             Int32Array.BYTES_PER_ELEMENT // 4
//             Uint32Array.BYTES_PER_ELEMENT // 4
//             Float32Array.BYTES_PER_ELEMENT // 4
//             Float64Array.BYTES_PER_ELEMENT // 8
//         这个属性在TypedArray实例上也能获取，即有 TypedArray.prototype.BYTES_PER_ELEMENT。
//     2.6、ArrayBuffer 与字符串的互相转换
//         ArrayBuffer 和字符串的相互转换，使用原生 TextEncoder 和 TextDecoder 方法。为了便于说明用法，下面的代码都按照 TypeScript 的用法，给出了类型签名。
//     2.7、溢出
//         不同的视图类型，所能容纳的数值范围是确定的。超出这个范围，就会出现溢出。比如，8 位视图只能容纳一个 8 位的二进制值，如果放入一个 9 位的值，就会溢出。
//         TypedArray 数组的溢出处理规则，简单来说，就是抛弃溢出的位，然后按照视图类型进行解释。
//         一个简单转换规则，可以这样表示。
//             正向溢出（overflow）：当输入值大于当前数据类型的最大值，结果等于当前数据类型的最小值加上余值，再减去 1。
//             负向溢出（underflow）：当输入值小于当前数据类型的最小值，结果等于当前数据类型的最大值减去余值的绝对值，再加上 1。
//     2.8、TypedArray.prototype.buffer
//         TypedArray实例的buffer属性，返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。
//     2.9、TypedArray.prototype.byteLength，TypedArray.prototype.byteOffset
//         byteLength属性返回 TypedArray 数组占据的内存长度，单位为字节。byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都是只读属性。
//     2.10、TypedArray.prototype.length
//         length属性表示 TypedArray 数组含有多少个成员。注意将 length 属性和 byteLength 属性区分，前者是成员长度，后者是字节长度。
//     2.11、TypedArray.prototype.set()
//         TypedArray 数组的set方法用于复制数组（普通数组或 TypedArray 数组），也就是将一段内容完全复制到另一段内存。
//     2.12、TypedArray.prototype.subarray()
//         subarray方法是对于 TypedArray 数组的一部分，再建立一个新的视图。
//     2.13、TypedArray.prototype.slice()
//         TypeArray 实例的slice方法，可以返回一个指定位置的新的TypedArray实例。
//     2.14、TypedArray.of()
//         TypedArray 数组的所有构造函数，都有一个静态方法of，用于将参数转为一个TypedArray实例。
//     2.15、TypedArray.from()
//         静态方法from接受一个可遍历的数据结构（比如数组）作为参数，返回一个基于这个结构的TypedArray实例。
// 3、复合视图
//     由于视图的构造函数可以指定起始位置和长度，所以在同一段内存之中，可以依次存放不同类型的数据，这叫做“复合视图”。
//         const buffer = new ArrayBuffer(24);
//         const idView = new Uint32Array(buffer, 0, 1);
//         const usernameView = new Uint8Array(buffer, 4, 16);
//         const amountDueView = new Float32Array(buffer, 20, 1);
//     上面代码将一个 24 字节长度的ArrayBuffer对象，分成三个部分：
//         字节 0 到字节 3：1 个 32 位无符号整数
//         字节 4 到字节 19：16 个 8 位整数
//         字节 20 到字节 23：1 个 32 位浮点数
// 4、DataView 视图
//     如果一段数据包括多种类型（比如服务器传来的 HTTP 数据），这时除了建立ArrayBuffer对象的复合视图以外，还可以通过DataView视图进行操作。
//     DataView视图提供更多操作选项，而且支持设定字节序。本来，在设计目的上，ArrayBuffer对象的各种TypedArray视图，是用来向网卡、声卡之类的本机设备传送数据，
//         所以使用本机的字节序就可以了；而DataView视图的设计目的，是用来处理网络设备传来的数据，所以大端字节序或小端字节序是可以自行设定的。
//     DataView视图本身也是构造函数，接受一个ArrayBuffer对象作为参数，生成视图。
//     new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

//     DataView实例有以下属性，含义与TypedArray实例的同名方法相同。
//         DataView.prototype.buffer：返回对应的 ArrayBuffer 对象
//         DataView.prototype.byteLength：返回占据的内存字节长度
//         DataView.prototype.byteOffset：返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
//     DataView实例提供 8 个方法读取内存。
//         getInt8：读取 1 个字节，返回一个 8 位整数。
//         getUint8：读取 1 个字节，返回一个无符号的 8 位整数。
//         getInt16：读取 2 个字节，返回一个 16 位整数。
//         getUint16：读取 2 个字节，返回一个无符号的 16 位整数。
//         getInt32：读取 4 个字节，返回一个 32 位整数。
//         getUint32：读取 4 个字节，返回一个无符号的 32 位整数。
//         getFloat32：读取 4 个字节，返回一个 32 位浮点数。
//         getFloat64：读取 8 个字节，返回一个 64 位浮点数。
//     DataView 视图提供 8 个方法写入内存。
//         setInt8：写入 1 个字节的 8 位整数。
//         setUint8：写入 1 个字节的 8 位无符号整数。
//         setInt16：写入 2 个字节的 16 位整数。
//         setUint16：写入 2 个字节的 16 位无符号整数。
//         setInt32：写入 4 个字节的 32 位整数。
//         setUint32：写入 4 个字节的 32 位无符号整数。
//         setFloat32：写入 4 个字节的 32 位浮点数。
//         setFloat64：写入 8 个字节的 64 位浮点数。
// 5、二进制数组的应用
//     大量的 Web API 用到了ArrayBuffer对象和它的视图对象。
//     5.1、AJAX
//         传统上，服务器通过 AJAX 操作只能返回文本数据，即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据，这时分成两种情况。
//         如果明确知道返回的二进制数据类型，可以把返回类型（responseType）设为arraybuffer；如果不知道，就设为blob。
//         let xhr = new XMLHttpRequest();
//         xhr.open('GET', someUrl);
//         xhr.responseType = 'arraybuffer';

//         xhr.onload = function () {
//         let arrayBuffer = xhr.response;
//         // ···
//         };

//         xhr.send();

//         如果知道传回来的是 32 位整数，可以像下面这样处理。

//         xhr.onreadystatechange = function () {
//         if (req.readyState === 4 ) {
//             const arrayResponse = xhr.response;
//             const dataView = new DataView(arrayResponse);
//             const ints = new Uint32Array(dataView.byteLength / 4);

//             xhrDiv.style.backgroundColor = "#00FF00";
//             xhrDiv.innerText = "Array is " + ints.length + "uints long";
//         }
//         }
//     5.2、Canvas
//         网页Canvas元素输出的二进制像素数据，就是 TypedArray 数组。
//     5.3、WebSocket
//         WebSocket可以通过ArrayBuffer，发送或接收二进制数据。
//     5.4、Fetch API
//         Fetch API 取回的数据，就是ArrayBuffer对象。
//     5.5、File API
//         如果知道一个文件的二进制数据类型，也可以将这个文件读取为ArrayBuffer对象。
// 6、SharedArrayBuffer
//     JavaScript 是单线程的，Web worker 引入了多线程：主线程用来与用户互动，Worker 线程用来承担计算任务。每个线程的数据都是隔离的，通过postMessage()通信。下面是一个例子。
//         // 主线程
//         const w = new Worker('myworker.js');
// 7、Atomics 对象
//     多线程共享内存，最大的问题就是如何防止两个线程同时修改某个地址，或者说，当一个线程修改共享内存以后，必须有一个机制让其他线程同步。SharedArrayBuffer API 
//         提供Atomics对象，保证所有共享内存的操作都是“原子性”的，并且可以在所有线程内同步。
//     什么叫“原子性操作”呢？现代编程语言中，一条普通的命令被编译器处理以后，会变成多条机器指令。如果是单线程运行，这是没有问题的；多线程环境并且共享内存时，就会出问题，
//         因为这一组机器指令的运行期间，可能会插入其他线程的指令，从而导致运行结果出错。请看下面的例子。
//     // 主线程
//     ia[42] = 314159;  // 原先的值 191
//     ia[37] = 123456;  // 原先的值 163
//     // Worker 线程
//     console.log(ia[37]);
//     console.log(ia[42]);
//     // 可能的结果
//     // 123456
//     // 191
//     Atomics对象提供多种方法。
//     （1）Atomics.store()，Atomics.load()
//         store()方法用来向共享内存写入数据，load()方法用来从共享内存读出数据。比起直接的读写操作，它们的好处是保证了读写操作的原子性。
//             Atomics.load(typedArray, index)
//             Atomics.store(typedArray, index, value)
//         store()方法接受三个参数：typedArray对象（SharedArrayBuffer 的视图）、位置索引和值，返回typedArray[index]的值。load()方法只接受两个参数：
//             typedArray对象（SharedArrayBuffer 的视图）和位置索引，也是返回typedArray[index]的值。
//     （2）Atomics.exchange()
//         Worker 线程如果要写入数据，可以使用上面的Atomics.store()方法，也可以使用Atomics.exchange()方法。它们的区别是，Atomics.store()返回写入的值，
//             而Atomics.exchange()返回被替换的值。
//     （3）Atomics.wait()，Atomics.notify()
//         使用while循环等待主线程的通知，不是很高效，如果用在主线程，就会造成卡顿，Atomics对象提供了wait()和notify()两个方法用于等待通知。这两个方法相当于锁内存，
//             即在一个线程进行操作时，让其他线程休眠（建立锁），等到操作结束，再唤醒那些休眠的线程（解除锁）。 
//         Atomics.notify()方法以前叫做Atomics.wake()，后来进行了改名。
//         Atomics.wait()方法的使用格式如下。
//         Atomics.wait(sharedArray, index, value, timeout)
//         它的四个参数含义如下。
//             sharedArray：共享内存的视图数组。
//             index：视图数据的位置（从0开始）。
//             value：该位置的预期值。一旦实际值等于预期值，就进入休眠。
//             timeout：整数，表示过了这个时间以后，就自动唤醒，单位毫秒。该参数可选，默认值是Infinity，即无限期的休眠，只有通过Atomics.notify()方法才能唤醒。
//         Atomics.notify()方法的使用格式如下。
//             Atomics.notify(sharedArray, index, count)
//             它的三个参数含义如下。
//                 sharedArray：共享内存的视图数组。
//                 index：视图数据的位置（从0开始）。
//                 count：需要唤醒的 Worker 线程的数量，默认为Infinity。
//     （4）运算方法
//         共享内存上面的某些运算是不能被打断的，即不能在运算过程中，让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法，防止数据被改写。
//         Atomics.add(sharedArray, index, value)   
//             Atomics.add用于将value加到sharedArray[index]，返回sharedArray[index]旧的值。
//         Atomics.sub(sharedArray, index, value)   
//             Atomics.sub用于将value从sharedArray[index]减去，返回sharedArray[index]旧的值。  
//         Atomics.and(sharedArray, index, value)  
//             Atomics.and用于将value与sharedArray[index]进行位运算and，放入sharedArray[index]，并返回旧的值。
//         Atomics.or(sharedArray, index, value)
//             Atomics.or用于将value与sharedArray[index]进行位运算or，放入sharedArray[index]，并返回旧的值。
//         Atomics.xor(sharedArray, index, value)
//             Atomic.xor用于将vaule与sharedArray[index]进行位运算xor，放入sharedArray[index]，并返回旧的值。        
//     （5）其他方法  ：Atomics对象还有以下方法。
//         Atomics.compareExchange(sharedArray, index, oldval, newval)：如果sharedArray[index]等于oldval，就写入newval，返回oldval。
//         Atomics.isLockFree(size)：返回一个布尔值，表示Atomics对象是否可以处理某个size的内存锁定。如果返回false，应用程序就需要自己来实现锁定。
            

// 二十四、装饰器（Decorator）
// 装饰器（Decorator）是一种与类（class）相关的语法，用来注释或修改类和类方法。许多面向对象的语言都有这项功能，目前有一个提案将其引入了 ECMAScript。
// 装饰器是一种函数，写成@ + 函数名。它可以放在类和类方法的定义前面。
// 1、类的装饰
//     装饰器可以用来装饰整个类。
//     @testable
//     class MyTestableClass {
//     // ...
//     }
// 2、方法的装饰
//     装饰器不仅可以装饰类，还可以装饰类的属性。
//     class Person {
//     @readonly
//     name() { return `${this.first} ${this.last}` }
//     }
//     装饰器函数readonly一共可以接受三个参数。
//     function readonly(target, name, descriptor){}
// 3、为什么装饰器不能用于函数？
//     装饰器只能用于类和类的方法，不能用于函数，因为存在函数提升。
// 4、core-decorators.js
//     core-decorators.js是一个第三方模块，提供了几个常见的装饰器，通过它可以更好地理解装饰器。
//     （1）@autobind
//         autobind装饰器使得方法中的this对象，绑定原始对象。
//     （2）@readonly
//         readonly装饰器使得属性或方法不可写。
//     （3）@override
//         override装饰器检查子类的方法，是否正确覆盖了父类的同名方法，如果不正确会报错。
//     （4）@deprecate (别名@deprecated)
//         deprecate或deprecated装饰器在控制台显示一条警告，表示该方法将废除。
//     （5）@suppressWarnings
//         suppressWarnings装饰器抑制deprecated装饰器导致的console.warn()调用。但是，异步代码发出的调用除外。
// 5、使用装饰器实现自动发布事件
//     我们可以使用装饰器，使得对象的方法被调用时，自动发出一个事件。
// 6、Mixin
//     在装饰器的基础上，可以实现Mixin模式。所谓Mixin模式，就是对象继承的一种替代方案，中文译为“混入”（mix in），意为在一个对象之中混入另外一个对象的方法。
// 7、Trait
//     Trait 也是一种装饰器，效果与 Mixin 类似，但是提供更多功能，比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
//     下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器，不仅可以接受对象，还可以接受 ES6 类作为参数。
//     import { traits } from 'traits-decorator';
//     class TFoo {
//     foo() { console.log('foo') }
//     }
//     const TBar = {
//     bar() { console.log('bar') }
//     };
//     @traits(TFoo, TBar)
//     class MyClass { }
//     let obj = new MyClass();
//     obj.foo() // foo
//    obj.bar() // bar